Link labels
This section explains everything you need to know to make full use of the powerful label system for links.
JointJS offers a full suite of methods for working with link labels:
link.labels(labels)
- sets thelabels
array of the link. If called without arguments, returns thelabels
array.link.label(index, label)
- sets the providedlabel
at the givenindex
. If called withindex
only, returns that label.link.insertLabel(index, label)
- inserts the providedlabel
at the givenindex
.link.appendLabel(label)
- shortcut function. Inserts thelabel
at the end of thelabels
array.link.removeLabel(index)
- removes the label atindex
.
You may provide an array of labels to the link through the labels property. Each label can have its own markup
, size
, attrs
, and position
objects specified. The values in those objects take precedence over any defaults which may apply on the label.
Default label
To avoid excessive repetition, you can provide a defaultLabel
property to the link, to set the markup
, size
, attrs
, and position
objects which should be applied to all labels on the link. The properties from defaultLabel
act as a template, which is overwritten / extended by individually-specified label properties as appropriate.
An example of creating a new link instance with both the defaultLabel
and labels
properties specified can be seen below.
import { shapes } from '@joint/core';
const link = new shapes.standard.Link({
source: { x: 50, y: 400 },
target: { x: 500, y: 400 },
defaultLabel: {
// applied to all labels on this link:
markup: [
{
tagName: 'rect',
selector: 'body'
}, {
tagName: 'text',
selector: 'label'
}
],
size: {
// used by `calc()` expressions in `attrs`
width: 150,
height: 30
},
attrs: {
body: {
width: 'calc(w)',
height: 'calc(h)',
// center around label position:
x: 'calc(w/-2)',
y: 'calc(h/-2)',
stroke: 'black',
fill: 'white'
},
label: {
textWrap: {
width: 'calc(w-5)',
height: 'calc(h-5)'
},
// center text around label position:
// (no `x` and `y` provided = no offset)
textAnchor: 'middle',
textVerticalAnchor: 'middle',
fontSize: 16,
fontFamily: 'sans-serif'
}
}
},
labels: [{
// specification of an individual label:
size: { width: 200 }, // partially overwrites `defaultLabel.size`
attrs: {
label: {
text: 'Hello World'
}
},
position: { distance: 0.25 } // overwrites built-in default
}]
});
Built-in default label
A simple label definition (including markup, attrs and position) is built into the dia.Link
class, from which all Link subtypes inherit it (including default shapes.standard.Link
). The built-in default label markup contains two subelements: <text>
SVGElement ('text'
selector) for label text, and <rect>
SVGElement ('rect'
selector) for label background. The built-in default attributes specify a simple vertical-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.appendLabel({
attrs: {
text: {
text: 'Hello, World!'
}
}
});
Details
Built-in default label properties
To ensure backwards compatibility, thejoint.dia.Link
class comes with a private built-in defaultLabel
property. It is reproduced here for reference:defaultLabel: {
// built-in default markup:
// applied only if neither one of the following is provided:
// - individual label `markup` property
// - `defaultLabel.markup` property
markup: [
{
tagName: 'rect',
selector: 'rect'
}, {
tagName: 'text',
selector: 'text'
}
],
// built-in default attributes:
// applied only if built-in default markup is used
attrs: {
text: {
fill: '#000000',
fontSize: 14,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
pointerEvents: 'none'
},
rect: {
ref: 'text',
fill: '#ffffff',
rx: 3,
ry: 3,
x: 'calc(x)',
y: 'calc(y)',
width: 'calc(w)',
height: 'calc(h)'
}
},
// built-in default position:
// merged with `defaultLabel.position` and individual label `position`
position: {
distance: 0.5
}
}
If custom markup
object is not provided (i.e. there is no class-specific defaultLabel.markup
object, nor any instance-specific defaultLabel.markup
, nor an individual label-specific markup
property), then built-in default label markup
is applied (as reproduced above). Alongside, the built-in default label attrs
object is applied. Note that the built-in default attrs
object is applied as a template in this context, which means that you may enhance it with a custom attrs
object (class-specific / instance-specific / individual label-specific). However, in the interest of keeping your code maintainable and easy to understand, it is very highly recommended that you provide both your own markup
object and your own attrs
object, unless you want to use the built-in default precisely as-is.
The built-in default position
object behaves slightly differently. Regardless of markup
, it is always merged with custom position
objects (class-specific / instance-specific / individual label-specific) - but it has the lowest priority of the four. That is, if at least one of the custom position
objects provides a distance
value, that value will have precedence over the built-in default position.distance
. If no custom position.distance
is provided, then the built-in default is applied (placing labels at midpoints of links).
Label position
Labels are positioned at the center point of the link (distance
of 0.5
) as a built-in default. Three kinds of label.position.distance
values are recognized for setting a custom position. A value between 0
and 1
causes the label to be positioned relatively to link length. Positive values signify absolute position in local SVG units away from the start point. Finally, negative values mean absolute position away from end point. An animated example is presented below. (Link labels can also be emulated with link subelements and special attributes; this technique is explained in another section).
link.appendLabel({
attrs: {
text: {
text: '0.25'
}
},
position: {
distance: 0.25
}
});
link.appendLabel({
attrs: {
text: {
text: '150'
}
},
position: {
distance: 150
}
});
link.appendLabel({
attrs: {
text: {
text: '-100'
}
},
position: {
distance: -100
}
});
Note that a label.position.distance
value is required for all labels. If a label.position.distance
value is not provided for an individual label, the defaultLabel.position.distance
value is taken from the definition of the Link subtype. If the Link subtype definition has no defaultLabel
object, or if its defaultLabel
has no position
property, the default built-in value of 0.5
is applied. Note that - if necessary - the default built-in position.distance
value will be mixined with the rest of the custom position
object you provide (offset
, angle
, args
).
Label offset
It is also possible to set label offsets. This is done with the label.position.offset
property. With a positive number, the label is offset relatively and to the right of the link (according to the source
-target
direction of the link); a negative number causes the label to be offset to the left. An object with x
and y
coordinates offsets the label absolutely by that amount in the two dimensions. The following example illustrates these three options. The red asterisk marks the reference point of all labels on the link.
link.appendLabel({
attrs: {
text: {
text: 'offset: 40'
}
},
position: {
distance: 0.66,
offset: 40
}
});
link.appendLabel({
attrs: {
text: {
text: 'offset: -40'
}
},
position: {
distance: 0.66,
offset: -40
}
});
link.appendLabel({
attrs: {
text: {
text: 'offset: -40,80'
}
},
position: {
distance: 0.66,
offset: {
x: -40,
y: 80
}
}
});
By default, the labels' anchor point is centered horizontally and vertically, as it was in this example. This can be changed by the native textAnchor
SVG attribute and by the JointJS special textVerticalAnchor
attribute, respectively.
Label rotation
Link labels are horizontal by default, but JointJS allows you to specify label rotation. If you provide a value for the label.position.angle
property, the link will rotate clockwise by that amount (regardless of the path of the link). If the label.position.args.keepGradient
boolean flag is set to true
, the label is first rotated so that its slope matches the slope of the connection path at the given position, and then the label is rotated further according to the label.position.angle
property (if any). Additionally, if the label.position.args.ensureLegibility
boolean flag is set to true
, it ensures that label text never ends up being upside-down - if necessary, JointJS adds an additional 180-degree rotation to make the text legible. The following example shows rotated links in action. The red asterisk marks the reference point of the two labels that are offset from the connection path.
link.appendLabel({
attrs: {
text: {
text: '70°\nkeepGradient'
}
},
position: {
distance: 0.05,
angle: 70,
args: {
keepGradient: true
}
}
});
link.appendLabel({
attrs: {
text: {
text: '0°\nkeepGradient'
}
},
position: {
distance: 0.3,
args: {
keepGradient: true
}
}
});
link.appendLabel({
attrs: {
text: {
text: '45°'
}
},
position: {
distance: 0.8,
angle: 45
}
});
link.appendLabel({
attrs: {
text: {
text: '135°'
}
},
position: {
distance: 0.9,
angle: 135
}
});
link.appendLabel({
attrs: {
text: {
text: '270°\nkeepGradient'
}
},
position: {
distance: 0.66,
offset: 80,
angle: 270,
args: {
keepGradient: true
}
}
});
link.appendLabel({
attrs: {
text: {
text: '270°\nkeepGradient\nensureLegibility'
}
},
position: {
distance: 0.66,
offset: -80,
angle: 270,
args: {
keepGradient: true,
ensureLegibility: true
}
}
});
Label styling
Of course, it is also possible to change the appearance of your labels. To specify custom markup, you may provide a markup
object in JSON format or as a util.svg
ES6 tag template. As a bonus, you can define custom selectors to identify individual components of your label. To specify custom presentation attributes, you may provide an attrs
object with native SVG attributes or JointJS special attributes. Finally, to specify of the label, you may provide a size
object with width and/or height properties. Let's define a complex circular label that shows what JointJS can do:
link.appendLabel({
markup: [
{
tagName: 'circle',
selector: 'body'
}, {
tagName: 'text',
selector: 'label'
}, {
tagName: 'circle',
selector: 'asteriskBody'
}, {
tagName: 'text',
selector: 'asterisk'
}
],
// no `size` object provided = calc() operations need `ref` property
attrs: {
label: {
text: '½',
fill: '#000000',
fontSize: 14,
textAnchor: 'middle',
yAlignment: 'middle',
pointerEvents: 'none'
},
body: {
// calc() is responsive to size of 'label':
ref: 'label',
fill: '#ffffff',
stroke: '#000000',
strokeWidth: 1,
r: 'calc(s)',
cx: 0,
cy: 0
},
asterisk: {
// calc() is responsive to size of 'label':
ref: 'label',
text: '*',
fill: '#ff0000',
fontSize: 8,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
pointerEvents: 'none',
x: 'calc(x+16.5)',
y: 'calc(y-2)'
},
asteriskBody: {
// calc() is responsive to size of 'asterisk':
ref: 'asterisk',
fill: '#ffffff',
stroke: '#000000',
strokeWidth: 1,
r: 'calc(s)',
cx: 'calc(x+calc(0.5*w))',
cy: 'calc(y+calc(0.5*h))'
}
}
});
We did not specify any size
object in our example, so JointJS was missing overarching reference width and height dimensions for use in the various calc()
operations inside the attrs
object. In order to use calc()
operations anyways, we instead provided ref
special attributes where necessary, to identify reference subelements from which each attrs
property could determine its dimensions. In our example, attrs/body
and attrs/asterisk
used the dimensions of the label
SVGTextElement for reference, while attrs/asteriskBody
used the dimensions of the asterisk
SVGTextElement for reference. (If we had specified neither size
nor an individual ref
attribute within the attrs
objects, the defaultLabel.size
from the definition of the Link subtype would have been used for reference instead. However, if there had been no defaultLabel
object in the Link subtype definition, or if its defaultLabel
had had no size
property, 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 - as is the case for the asteriskBody
subelement which calculates its position by reference to the asterisk
subelement.
Interaction
By default, users cannot interact with link labels in any way. However, you can enable label dragging for all labels with the paper.options.interactive
paper option:
const paper = new dia.Paper({
// ...
interactive: {
linkMove: false,
labelMove: true,
arrowheadMove: false,
vertexMove: false,
vertexAdd: false,
vertexRemove: false,
useLinkTools: false
}
});
The new position of dragged labels is recorded relatively to the link path. That means that the label will reposition itself when the link changes. (Unfortunately, it also means that the label can never be dragged beyond the endpoints of the link).
If you only want to allow the label to be dragged along the length of the link (and not outside of it), you can do so by specifying the paper.options.snapLabels
paper option:
const paper = new joint.dia.Paper({
// ...
snapLabels: true,
interactive: {
linkMove: false,
labelMove: true,
arrowheadMove: false,
vertexMove: false,
vertexAdd: false,
vertexRemove: false,
useLinkTools: false
}
});
Frequently asked questions
How can I wrap the text of a link label?
In general, text in JointJS is wrapped using the textWrap attribute. The same is true for the text of a link label.
Learn more...
What are the constraints on the text wrapping in your use case?
-
If you are limited by both width and height of your link label (the more common case), refer to the first question below.
-
If your link labels are only limited by width (but are allowed to expand vertically as necessary to contain all text), refer to the second question below.
How can I fit link label text into a rectangle of preset size?
You can specify the textWrap
attribute of your label text subelement with the ellipsis
option enabled, and with width
and height
options defined as calc()
expressions (referring to the label root's size
property).
Learn more...
In the following example, the label text subelement has selector labelText
while the label body subelement has selector labelBody
.
Neither width nor height of the label ever change. Instead, if the provided text
is too long to fit within the label area, an ellipsis is shown.
How can I limit the maximum text width inside a link label?
You can specify the textWrap
attribute of your label text subelement with the width
option defined as a constant value and height
options defined as null
, and you can use the ref
attribute to tell the label body subelement to refer to the label text subelement's dimensions when calculating its dimensions.
Learn more...
In the following example, the label text subelement has selector labelText
while the label body subelement has selector labelBody
.
labelText
determines the dimensions of labelBody
dynamically. The provided value of text
wraps into multiple lines if it cannot fit into the provided textWrap.width
.
Note that the size
property of label root is not used - linkText
calculates its own size via the textWrap
attribute, and linkBody
calculates its size by reference to linkText
.