Skip to main content

Custom shapes

JointJS comes with several collections of built-in element shapes. You can find more information on using these built-in shapes in this section.

However, even though there are many default shapes to choose from, you might find that you are missing one and want to create your own shape definition. Creating new shapes is very simple in JointJS if you already know SVG. The most important SVG elements are rect, text, circle, ellipse, image and path. Their thorough description with examples and illustrations can be found elsewhere on the Internet, e.g. on MDN. What is important for us is to realize that combining the basic SVG shapes, we can define any 2D shape we need.

If you like video, the following YouTube content will show you how to create custom JointJS elements using SVG via the Cell.define() function. This tutorial will still provide you with a comprehensive guide to defining custom shapes, even if you decide to watch the video or not.

A basic shape​

There are two primary ways to create a basic shape in JointJS:

Although all shapes are ultimately descendants of the dia.Cell class, you should inherit from the more specific dia.Element or dia.Link classes, which provide basic element and link functionality out-of-the-box, respectively. In practice, it usually makes the most sense for your shapes to inherit from a shape in one JointJS's predefined shape collections (e.g. shapes.standard.Rectangle). Of course, your shapes can also inherit from any other of your previously-defined custom shapes.

note

JointJS is fully compatible with TypeScript and you can create your custom shapes using TypeScript classes. The following examples are written in JavaScript, but can be used in TypeScript as well. We provide TypeScript definitions for all JointJS classes and functions, so you can use them in your projects without any issues.

We suggest that you use ES6 classes for defining new shapes, as it is more flexible and allows you to use the full power of JavaScript or TypeScript. However, if you prefer the Cell.define() function, you can use it as well. We explain the differences of this approach separately for each section of this tutorial - look for details boxes like the following:

Using Cell.define()

You can use Cell.define() function to declare custom shapes. Similarly to the ES6 class approach, you can call the define() function on dia.Element or dia.Link classes, or on another shape definition (e.g. a predefined JointJS shape like shapes.standard.Rectangle, or any custom shape you have previously defined).

The Cell.define() function can be called with up to four parameters. Here is how they map to building blocks of ES6 shapes:

  • [required] type - a string specifying the type of the new shape.

  • [optional] defaultInstanceProperties - an object that contains properties to be assigned to every constructed instance of the shape:

  • [optional] prototypeProperties - an object that contains properties to be assigned on the shape's prototype. Intended for properties which are intrinsic to the shape definition itself and not usually changed on individual instances:

  • [optional] staticProperties - an object that contains properties to be assigned on the shape's constructor (not very common):

Let's define a basic custom shape using ES6 classes:

import { dia } from '@joint/core';

export class MyShape extends dia.Element {

defaults() {
return {
...super.defaults
};
}
}

The defaults function returns an object with the default model attributes of our shape. It is possible to use an object for our defaults, but since objects are referenced - not copied - in JavaScript, our function will return a different object each time.

In JavaScript classes, the super keyword allows us to access the properties of the parent class definition. Using ...super.defaults (with a spread operator) thus makes it possible for our shape definition (MyShape) to inherit all optional model attributes from its parent shape definition (dia.Element). More precisely, if a model attribute is left undefined on the child, the definition of the attribute from the parent class is assigned instead, and so on up the inheritance chain. Conversely, providing a value to a model attribute on the child completely overrides any value of the same property from the parent. If you need parent properties to be merged with child properties instead, use the util.defaultsDeep() function:

Using util.defaultsDeep()

Instead of replacing parent properties outright, the util.defaultsDeep() function recursively assigns parent properties as default properties. This makes a difference for object properties - if the parent's object has some extra properties which are not defined inside the child's object, those are merged into the child object.

Our custom shape definition would look like the following:

import { dia, util } from '@joint/core';

export class MyShape extends dia.Element {

defaults() {
return util.defaultsDeep({}, super.defaults);
}
}

This is the most basic boilerplate for a custom shape, but you have to agree it's not very exciting, so let's add some more default model attributes to our shape.

Model attributes​

You most probably need some default model attributes to be specified for your custom shape. The default model attributes are used as a template for every instance of your custom shape. They are defined inside the defaults() function of your custom shape class.

Type​

The most important model attribute is type. It specifies a unique path identifier within a cellNamespace object. Cell namespaces allow JointJS to find the correct constructor for each encountered object, which becomes crucially important when importing diagrams from JSON - JointJS references the Graph's cellNamespace object according to each saved object's type path identifier. You can find more information about cell namespaces in the Cell namespaces section of our documentation.

Let's modify our MyShape class to include a type attribute:

import { dia, shapes } from '@joint/core';

export class MyShape extends dia.Element {

defaults() {
return {
...super.defaults,
type: 'myNamespace.MyShape'
};
}
}
Using util.defaultsDeep()

With the util.defaultsDeep() function, our modified custom shape definition would look like the following:

import { dia, util } from '@joint/core';

export class MyShape extends dia.Element {

defaults() {
return util.defaultsDeep({
type: 'myNamespace.MyShape'
}, super.defaults);
}
}
Using Cell.define()

With the Cell.define() approach, our modified custom shape definition would look like the following:

import { dia } from '@joint/core';

export const MyShape = dia.Element.define('myNamespace.MyShape');

Here is an example of how our custom shape could then be added to our Graph's cellNamespace object:

import { shapes, dia } from '@joint/core';
import { MyShape } from './shapes';

const namespace = { ...shapes, myNamespace: { MyShape }};

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

Optional model attributes​

Besides the type attribute, you may want to specify default values for various optional model attributes of your custom shape. We have already set up our shape to receive parent attributes as defaults, so we should now add some unique attributes which would set our custom shape apart from its parent and other shapes.

Elements and Links have their own special model attributes like size (illustrated below), source and router (both specific to Links), and others. You can learn more about model attributes in dedicated element and link sections of our documentation. However, the most interesting model attribute to talk about here is attrs - an object which allows us to specify presentation attributes for our shape.

Within the attrs object, the keys identify different subelement selectors, as defined in the shape markup - in our example, the respective keys are body and label. For each of these subelements, we may provide the values for different presentation attributes - those can be standard SVG attributes, or special JointJS presentation attributes that provide additional functionality.

Let's modify our MyShape class again to include some additional model attributes:

import { dia, util } from '@joint/core';

export class MyShape extends dia.Element {

defaults() {
return {
...super.defaults,
type: 'myNamespace.MyShape',
size: { width: 100, height: 80 },
attrs: {
body: {
cx: 'calc(0.5*w)',
cy: 'calc(0.5*h)',
rx: 'calc(0.5*w)',
ry: 'calc(0.5*h)',
strokeWidth: 2,
stroke: '#333333',
fill: '#FFFFFF'
},
label: {
textVerticalAnchor: 'middle',
textAnchor: 'middle',
x: 'calc(0.5*w)',
y: 'calc(0.5*h)',
fontSize: 14,
fill: '#333333'
}
}
};
}
}
Using util.defaultsDeep()

With the util.defaultsDeep() function, our modified custom shape definition would look like the following:

import { dia, util } from '@joint/core';

export class MyShape extends dia.Element {

defaults() {
return util.defaultsDeep({
type: 'myNamespace.MyShape',
size: { width: 100, height: 80 },
attrs: {
body: {
cx: 'calc(0.5*w)',
cy: 'calc(0.5*h)',
rx: 'calc(0.5*w)',
ry: 'calc(0.5*h)',
strokeWidth: 2,
stroke: '#333333',
fill: '#FFFFFF'
},
label: {
textVerticalAnchor: 'middle',
textAnchor: 'middle',
x: 'calc(0.5*w)',
y: 'calc(0.5*h)',
fontSize: 14,
fill: '#333333'
}
}
}, super.defaults);
}
}
Using Cell.define()

With the Cell.define() approach, our modified custom shape definition would look like the following:

import { dia } from '@joint/core';

export const MyShape = dia.Element.define('myNamespace.MyShape', {
size: { width: 100, height: 80 },
attrs: {
body: {
cx: 'calc(0.5*w)',
cy: 'calc(0.5*h)',
rx: 'calc(0.5*w)',
ry: 'calc(0.5*h)',
strokeWidth: 2,
stroke: '#333333',
fill: '#FFFFFF'
},
label: {
textVerticalAnchor: 'middle',
textAnchor: 'middle',
x: 'calc(0.5*w)',
y: 'calc(0.5*h)',
fontSize: 14,
fill: '#333333'
}
}
});

Markup​

The markup is a special prototype property which is specified as a JSON array. Each member of the array is taken to define one subelement of the new shape. Subelements are defined with objects containing a tagName (a string with the SVG tag name of the subelement) and a selector (a string identifier for this subelement in the shape). Although JointJS can also understand SVG markup in string form, that approach is noticeably slower due to the need for parsing and lack of capacity for custom selectors.

The recommended way to specify markup is via the util.svg() tagged template. This function is a helper that allows you to write SVG markup in a more readable way while keeping the performance benefits of markup specified as a JSON array.

The markup is treated as a prototype property and should exist before the element is instantiated. To achieve this, you should declare your markup in special preinitialize() function. Let's add markup to our custom shape:

note

We are declaring markup in a separate variable first in order to avoid creating a new instance of the template for each new instance of our shape. This is a good practice which avoids unnecessary memory usage and increases performance.

import { dia, util } from '@joint/core';

const markup = util.svg/* xml */`
<ellipse @selector="body"/>
<text @selector="label"/>
`;

export class MyShape extends dia.Element {

preinitialize() {
this.markup = markup;
}

// defaults() {}
}
Using Cell.define()

With the Cell.define() approach, markup is added inside the prototypeProperties parameter:

import { dia, util } from '@joint/core';

const markup = util.svg/* xml */`
<ellipse @selector="body"/>
<text @selector="label"/>
`;

export const MyShape = dia.Element.define('myNamespace.MyShape', {
// Optional model attributes
}, {
markup: markup
});

As mentioned above, the selectors defined in the markup can be used in the shape definition to specify values of different presentation attributes, but they can also be referenced later during program execution to target SVG subelements in order to get or set specific presentation attributes on an instance of our shape:

// create namespace, graph and paper
// ...

// create an instance of MyShape
const myShape = new MyShape();
myShape.addTo(graph);

// set the <rect/> color to red
myShape.attr('body/fill', 'red');

Methods​

Sometimes, the built-in functionality of JointJS is not enough, and custom behavior needs to be supported by your shape. In most of these cases, your use case can be addressed by prototype methods. Prototype methods are useful for custom initialization logic and for custom logic that reacts to user interaction (e.g. property validation) - in general, for any logic which needs access to the current instance of your shape.

For example, if we wanted to create a shortcut to assign a text label value to instances of our shape, we could create a setText() prototype method taking a string as its parameter and setting the label text attribute accordingly. Returning this is a good practice, as it allows chaining of prototype methods (in this example, the cell.attr() setter returns this):

import { dia } from '@joint/core';

export class MyShape extends dia.Element {

// preinitialize() {}

// defaults() {}

setText(text = '') {
return this.attr('label/text', text);
}
}
Using Cell.define()

With the Cell.define() approach, prototype methods are added inside the prototypeProperties parameter (next to markup):

import { dia } from '@joint/core';

export const MyShape = dia.Element.define('myNamespace.MyShape', {
// Optional model attributes
}, {
// Markup

setText(text = '') {
return this.attr('label/text', text);
}
});

Here is an example of how the setText() prototype method could be used to initialize the label text of an instance of our shape:

// create namespace, graph and paper
// ...

// create an instance of MyShape and set its text
const myShape = new MyShape().setText(text);

Static methods​

Static methods are occasionally useful to implement functionality which does not need a specific instance of a shape to operate.

For example, if we wanted to have a shortcut to check if an object of an unknown type is an instance of MyShape, we could create a static method MyShape.isMyShape():

import { dia } from '@joint/core';

export class MyShape extends dia.Element {

// preinitialize() {}

// defaults() {}

// Prototype methods

static isMyShape(obj) {
return obj instanceof MyShape;
}
}
Using Cell.define()

With the Cell.define() approach, static methods are added inside the staticProperties parameter (next to attributes):

import { dia } from '@joint/core';

export const MyShape = dia.Element.define('myNamespace.MyShape', {
// Optional model attributes
}, {
// Markup

// Prototype methods
}, {
isMyShape(obj) {
return obj instanceof MyShape;
}
});

Here is an example of how the MyShape.isMyShape() static method could be used to get an array of all MyShape instances in our graph:

// create namespace, graph and paper
// ...

// populate graph with various elements
// ...

// filter graph's contents to get an array of MyShape instances
const myShapes = graph.getCells().filter((cell) => MyShape.isMyShape(cell));

Custom Views​

Custom views provide some extra flexibility when it comes to working with our models. If we want some additional behavior, but don't believe that behavior should live alongside the presentational attributes on our models, custom views can provide this for us. Maybe you want to capture user input, see a minimal version of the same graph, apply some interesting effect to your elements; those are just a few of the reasons you may want to explore custom views.

In the following example, we create a fade view effect to change the opacity of MyShape elements:

import { dia, shapes, util } from '@joint/core';

const markup = util.svg/* xml */`
<rect @selector="body" cursor="pointer"/>
`

class MyShape extends dia.Element {

preinitialize() {
this.markup = markup;
}

defaults() {
return {
...super.defaults,
type: 'myShapeGroup.MyShape',
size: { width: 100, height: 30 },
position: { x: 10, y: 10 },
attrs: {
body: {
width: 'calc(w)',
height: 'calc(h)'
}
}
}
}
}

const MyShapeView = dia.ElementView.extend({

// Make sure that all super class presentation attributes are preserved
presentationAttributes: dia.ElementView.addPresentationAttributes({
// mapping the model attributes to flag labels
faded: 'flag:opacity'
}),

confirmUpdate(flags, ...args) {
dia.ElementView.prototype.confirmUpdate.call(this, flags, ...args);
if (this.hasFlag(flags, 'flag:opacity')) this.toggleFade();
},

toggleFade() {
this.el.style.opacity = this.model.get('faded') ? 0.5 : 1;
}
});

const graph = new dia.Graph({}, { cellNamespace: shapes });
const paper = new dia.Paper({
model: graph,
width: 800,
height: 800,
gridSize: 1,
interactive: true,
async: true,
frozen: false,
background: { color: '#F3F7F6' },
cellViewNamespace: shapes
});
document.getElementById('canvas').appendChild(paper.el);

Object.assign(shapes, {
myShapeGroup: {
MyShape,
MyShapeView
},
standard: {
Rectangle: shapes.standard.Rectangle
}
});

graph.fromJSON({
cells: [
{ type: 'myShapeGroup.MyShape'},
{
type: 'standard.Rectangle',
size: { width: 40, height: 40 },
position: { x: 40, y: 50 }
}
]
});

graph.getElements()[0].set('faded', true);

The custom view implementation looks quite similar to our custom namespace example from earlier, but with a few key differences. The first major difference is that we define our custom view using dia.ElementView.extend({}). When the custom view is defined, it will listen to the underlying model changes, and update itself. When we are satisfied with the functionality we have created in our view, we then need to set up the namespace correctly once more. In our example, we do this by extending our namespace with MyShapeView. The Paper will search for any model types with a suffix of 'View' in our namespace. As we have completed the set up, that means the behavior defined in our custom view is now available for us to use how we want.

A more powerful alternative is to override the default view found in the namespace. Which approach you use will depend on your specific needs. To override the default view, we use the elementView setting in our Paper options. You can see that in action below.

const MyShapeView = dia.ElementView.extend({

// Custom view functionality

});

const graph = new dia.Graph({}, { cellNamespace: shapes });
const paper = new dia.Paper({

// Options

elementView: () => MyShapeView
});

Custom elements example​

Let's apply everything we learned so far and create two custom elements: Node class based on dia.Element and ButtonNode element based on Node shape, which contains HTML element inside (more on this topic in our HTML inside shapes section). Keep in mind that the provided instance properties are mixined with the parent definition, but prototype and static properties are not. This means that it is enough for us to record only the attributes that changed in the definition of the custom element subtype (however, if we wanted to change the markup, we would have to do so explicitly).

import { dia, shapes, util } from '@joint/core';

const nodeMarkup = util.svg/* xml */`
<rect @selector="nodeBody"/>
<clipPath @selector="clipPath"><rect @selector="clipPathRect"/></clipPath>
<rect @selector="nodeHeader"/>
<text @selector="nodeHeaderLabel"/>
<text @selector="label"/>
`

class Node extends dia.Element {
preinitialize() {
this.markup = nodeMarkup;
}

defaults() {
const clipId = util.uuid();

return {
...super.defaults,
type: 'examples.Node',
size: {
width: 150,
height: 80
},
attrs: {
root: {
cursor: 'move'
},
nodeBody: {
width: 'calc(w)',
height: 'calc(h)',
fill: '#023047',
rx: 6
},
nodeHeaderLabel: {
x: 15,
y: 20 / 2 + 1,
textAnchor: 'start',
textVerticalAnchor: 'middle',
fill: '#023047',
text: 'Node',
fontSize: 10
},
nodeHeader: {
x: 0,
y: 0,
width: 'calc(w)',
height: 20,
clipPath: `url(#${clipId})`,
fill: '#ffb703'
},
clipPath: {
id: clipId
},
clipPathRect: {
width: 'calc(w)',
height: 'calc(h)',
rx: 6,
},
label: {
x: 'calc(w/2)',
y: 'calc(h/2+10)',
textAnchor: 'middle',
textVerticalAnchor: 'middle',
fill: 'white'
}
}
}
}
}

We add our shapes to the graph in the same manner described in our quick start tutorial. In the code below, we add a examples.Node shape with custom color and a multiline label. The multiline label respects the x and y values that we calculated with the calc() function earlier.

const n2 = new Node();
n2.position(50, 125);
n2.resize(200, 100);
n2.attr({
nodeBody: {
fill: '#219ebc'
},
label: {
text: 'Node\nanother color'
}
});
n2.addTo(graph);

To create ButtonNode shape, we will extend our Node class and add a button element to the markup. Notice, that we concatenate the markup property of the parent class with the new markup for the button element. To style the HTML inside the element we are using CSS classes which we can setup in the markup or in the attrs property.

import { dia, shapes, util } from '@joint/core';

const buttonNodeMarkup = util.svg/* xml */`
<foreignObject @selector="foreignObject">
<div class="button-container" @selector="buttonContainer">
<button class="button" @selector="button"></button>
</div>
</foreignObject>
`;

class ButtonNode extends Node {
preinitialize() {
super.preinitialize();

this.markup = this.markup.concat(buttonNodeMarkup);
}

defaults() {
const clipId = util.uuid();

return util.defaultsDeep({
type: 'examples.ButtonNode',
buttonTextVisible: 'Hide text',
buttonTextHidden: 'Show text',
attrs: {
nodeHeaderLabel: {
text: 'Button node'
},
label: {
y: 'calc(h/2+25)',
visibility: 'hidden'
},
foreignObject: {
x: 0,
y: 0,
width: 'calc(w)',
height: 'calc(h)',
}
}
}, super.defaults());
}
}

To implement listen to button events we are creating custom view for our ButtonNode shape:

export class ButtonNodeView extends dia.ElementView {

render() {
super.render();
this.el.getElementsByTagName('button')[0].textContent =
this.model.get('buttonTextHidden');
}

events() {
return {
'click button': (evt) => { this.onButtonClick(evt) }
}
}

onButtonClick(evt) {
const visibility = this.model.attr('label/visibility');
if (visibility === 'hidden') {
this.model.attr('label/visibility', 'visible');
evt.target.textContent = this.model.get('buttonTextVisible');
} else {
this.model.attr('label/visibility', 'hidden');
evt.target.textContent = this.model.get('buttonTextHidden');
}
}
}

Here we are listening for a click event on a button element. When the button is clicked, we change the visibility of the label text and the text of the button.

note

If you want to learn more about creating custom shapes using Typescript, you can find more information in the Typescript integration section.

The following example shows the default look of our Node and ButtonNode shapes with different attributes.

Default label​

When creating new links you may want to change default link label attributes. The second argument of the define() function (default instance properties) is also where defaultLabel for custom Link subtypes may be specified. This allows you to provide custom default markup, size, attrs and position for labels that are created on an instance of your custom Link type. The defaultLabel property is explained in separate section.

The shapes.standard.Link does not define its own custom default label, so the built-in default markup, attributes and position are used unless they are overridden by individual label markup, attrs and position.

The defaultLabel accepts four optional properties:

  • markup - sets the default markup of labels created on this Link subtype. Expected in the JSON markup format or as a util.svg ES6 tag template. (An SVG-parsable string is also accepted, but it is slower.).
  • size - sets the default dimensions of labels created on this Link subtype.
  • attrs - sets the default attributes of label subelements on this Link subtype. Uses selectors defined in markup.
  • position - sets the default position along the line at which labels will be added for instances of this Link subtype. The default is {distance: 0.5} (midpoint of the connection path).

Link labels are explained in depth in a separate section.

Example​

Let's apply our knowledge and create two custom links: Connection link based on standard.Link class and TwoWayConnection link based on the Connection shape.

const defaultLabelMarkup = util.svg/* xml */`
<rect @selector="body"/>
<text @selector="label"/>
`;

class Connection extends shapes.standard.Link {

defaults() {
return util.defaultsDeep({
type: 'examples.Connection',
defaultLabel: {
markup: defaultLabelMarkup,
attrs: {
label: {
fill: 'black', // default text color
fontSize: 12,
textAnchor: 'middle',
yAlignment: 'middle',
pointerEvents: 'none'
},
body: {
ref: 'label',
fill: 'white',
stroke: 'cornflowerblue',
strokeWidth: 2,
width: 'calc(1.2*w)',
height: 'calc(1.2*h)',
x: 'calc(x-calc(0.1*w))',
y: 'calc(y-calc(0.1*h))'
}
},
position: {
distance: 100, // default absolute position
args: {
absoluteDistance: true
}
}
},
attrs: {
line: {
stroke: 'cornflowerblue',
strokeWidth: 5,
targetMarker: {
'type': 'rect',
'width': 10,
'height': 20,
'y': -10,
'stroke': 'none'
}
}
},
}, super.defaults);
}
};

class TwoWayConnection extends Connection {

defaults() {
return util.defaultsDeep({
type: 'examples.TwoWayConnection',
attrs: {
line: {
sourceMarker: {
'type': 'rect',
'width': 10,
'height': 20,
'y': -10,
'stroke': 'none'
}
}
},
}, super.defaults());
}
};

We did not specify any defaultLabel.size object in our example, so JointJS was missing overarching reference width and height dimensions for use in the various attrs/body calc() operations. In order to use calc() operations anyways - specifically on attrs/body - we instead provided a ref special attribute to identify a reference subelement from which the dimensions could be determined. In our example, the reference subelement was the label SVGTextElement, so JointJS used its dimensions for the calc() operations inside attrs/body. (If we had specified neither defaultLabel.size nor an individual ref attribute on attrs/body, however, the calculations would have used 0 as the reference width and height, which would have been unexpected.)

Additionally, note that in our example the x and y position of the body subelement is also determined by reference to the label subelement - via calc() operations which refer to the x and y variables.

The following example shows our Connection and TwoWayConnection links in action:

Frequently asked questions​

Can I have two labels on an element?​

Absolutely. You can have as many labels as you need on an element - see below. Similarly, you can also have any number of labels on links and ports.

How can I give multiple labels to an element?​

You can give multiple labels to an element by creating a new element type with extra label subelements inside its markup property.

Learn more...
info

Element labels are part of the element type definition. This is a difference from links, which have a built-in API for adding and removing labels (to any link type).

In this example, the new element type is called TwoLabelsElement, and the two <text/> label subelements have selectors label1 and label2:

It is possible to define custom JointJS elements with hyperlinks pointing to external sites - by including the <a> SVG element in the markup of a custom element. The navigation path is then specified in individual elements' attributes of the custom type.

Learn more...

The two most important XLink attributes in this context are the following:

  • xlink:href - set the target resource URL.
  • xlink:show - how should the linked content be presented? Possible values are 'replace' (show in current window; default) and 'new' (show in new window).

In the built-in link types, the arrowhead does not capture pointer events because it's made using <marker/> SVGElements (which are not a part of the DOM tree). You can get around this limitation by specifying a custom link arrowhead as part of the link's markup - see below.

You can make a link's arrowhead clickable by creating a new link type with an extra <path/> subelement inside its markup property (instead of the usual approach which relies on JointJS marker attributes, like sourceMarker, targetMarker, vertexMarker).

Learn more...

The example utilizes the atConnectionRatio attribute to position the arrowhead subelement (selector: targetArrowhead) and event attribute to fire a custom event.

You can give a link two colors by defining a new link type with custom logic for the stroke-dasharray attribute.

Learn more...

In this example, we define a custom link type with two extra subelements (selectors: first and second) to be colored, differentiated by the custom index property. We refer to these index values inside the custom indexAttributeSetter() function, which calculates the stroke-dasharray value for each subelement based on the current length of the Link.

tip

To find out more about defining and using custom attributes, see the Working with model attributes section in the JointJS documentation.