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.
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.
refWidth
and
refHeight
- sets
the width of the subelement relative to model bbox.refX
and
refY
- sets the
coordinates of the top-left corner of the subelement relative to the top-left corner of model
bbox.
Percentages are interpreted relative to model bbox.x
/y
attribute.
refCx
and
refCy
- sets the
coordinates of the circle/ellipse center.
Percentages are interpreted relative to model bbox.refX
/refY
.
refRx
and
refRy
- sets the
radius of the ellipse relative to model bbox dimensions.
Percentages are interpreted relative to model bbox.
Note that for backwards compatibility, setting '100%'
here means that the radius
will be 100% of model size while the diameter of the ellipse in that direction will be 200% of model
size.
Thus, if you want the ellipse to fit into the model, use '50%'
.refR
- sets the radius
of the circle relative to the length of the shorter side of model bbox.
Percentages are interpreted relative to model bbox.
Note that for backwards compatibility, setting '100%'
here means that the radius
will be 100% of the length of model side.
If you want the circle to fit inside the model, use '50%'
.
There is also
refRCircumscribed
,
which sets the radius of the circle relative to the longest diagonal of model bbox.
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
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:
ref
- a selector
reference to the subelement whose measured bbox should be used as the base of relative
calculations.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:
sourceMarker
and
targetMarker
-
sets an arrowhead of requested SVGElement 'type'
to the start/end of the link.
The other properties passed in the object are expected to be SVG attributes for that
SVGElement.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:
connection
- if
set to true
, the subelement will follow the link connection path.
Applicable only to <path>
subelements.atConnectionLength
- sets the absolute distance from the beginning of the path at which the anchor of the subelement
should be placed.
Negative distances are counted from the end of the connection path.
Rotates subelement according to link gradient at requested length.atConnectionRatio
- sets the relative distance from the beginning of the path at which the anchor of the subelement
should be placed.
Accepts numbers between 0
and 1
.
Rotates subelement according to link gradient at requested ratio.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:
ref
attribute.
This means that they are unable to automatically set their width and height values based on the
browser-calculated dimensions of the text bbox.
It is up to the programmer to provide approximate width and height values to accommodate the label
text.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:
atConnectionLengthIgnoreGradient
- sets the absolute distance from the beginning of the path at which the anchor of the subelement
should be placed.
Negative distances are counted from the end of the connection path.
Does not rotate subelement according to path gradient.atConnectionRatioIgnoreGradient
- sets the relative distance from the beginning of the path at which the anchor of the subelement
should be placed.
Accepts numbers between 0
and 1
.
Does not rotate subelement according to path gradient.(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.