Link model attributes
Beside the model attributes which are common for all shapes, links have its own set of unique model attributes.
Source and target attributes
Links have two crucial model attributes: source
and target
. They define the starting point and the end point of the link. They can be defined with a Cell id (optionally, with additional subelement/magnet/port reference) or with a Point:
import { dia, shapes } from '@joint/core';
// `shapes.standard.Link` inherits from `dia.Link` (`dia.Link` is an abstract class that has no SVG markup defined)
const link1 = new shapes.standard.Link({
source: { id: sourceId },
target: { id: targetId, port: portId }
});
const link2 = new shapes.standard.Link({
source: { id: sourceId },
target: { x: 100, y: 100 }
});
If the end is specified as a Cell, a specific subelement on that cell may be identified for use as the link end. The selector
property allows specifying the subelement with a selector string, while the magnet
property uses magnet id, and the port
property (on elements only) uses the port id.
link.source(rect, {
selector: 'connectorSquare'
});
link.source(link2, {
selector: 'midPointCircle'
});
The source
and target
attributes accept additional modifier properties that modify the actual position of the link end: anchor
/linkAnchor
, and connectionPoint
.
In any case, we need to obtain a single point from the object provided to the source/target attribute. That point is found according to additional properties provided to the function. The accepted modifier properties depend on the class of the provided object:
Point
or an object withx
andy
properties - coordinates of the point are used as the coordinates of the link's anchor directly. No modification of any kind. (Ignoresanchor
/linkAnchor
/connectionPoint
properties, if provided.)Element
- the precise position of the link's end anchor depends on usedanchor
property, with additional optical modifications applied byconnectionPoint
property. (IgnoreslinkAnchor
property, if provided.)Link
- the precise position of the link's end anchor depends on providedlinkAnchor
function. (Ignoresanchor
/connectionPoint
properties, if provided.)Link
with specifiedsubelement
/magnet
property - same asElement
(see above).
The connectionStrategy
paper option is also relevant to mention in this context. It determines what happens to the link end when it is modified due to specific kinds of user interaction.
Anchor
If the link end (source/target) is an Element (or a subelement/magnet of a Link – but not a Link itself), the precise position of the link end's anchor may be specified by the anchor
property. Every link has two anchors; one at the source end and one at the target end.
A link end anchor is a point inside a given (sub)element that the link path wants to connect to at that source/target end - if it were not obstructed by the body of the element itself (it is the role of connectionPoints
to then take the obstructing end element itself into account). Alongside link vertices
, source and target anchors determine the basic link path. Then, it is the job of connectionPoints
, routers
, and connectors
to modify that path to make it look good.
The anchor functions reference the end element to find the anchor point - e.g. the center of the end element, or its top-right corner. Notably, anchor functions also allow you to offset found anchor points by a custom distance in both dimensions from the standard position (e.g. 10 pixels to the right and 20 pixels below the center of an end element). Several pre-made anchors are provided inside the JointJS library in the anchors
namespace.
link.source(rect, {
anchor: {
name: 'bottomLeft',
args: {
dx: 20,
dy: -10
}
}
});
There are many built-in anchor functions in JointJS:
'center'
- default anchor at center of view bbox.'modelCenter'
- anchor at center of model bbox.'perpendicular'
- anchor that ensures an orthogonal route to the other endpoint.'midSide'
- anchor in the middle of the side of view bbox closest to the other endpoint.'bottom'
- anchor in the middle of the bottom side of view bbox.'left'
- anchor in the middle of the left side of view bbox.'right'
- anchor in the middle of the right side of view bbox.'top'
- anchor in the middle of the top side of view bbox.'bottomLeft'
- anchor at the bottom-left corner of view bbox.'bottomRight'
- anchor at the bottom-right corner of view bbox.'topLeft'
- anchor at the top-left corner of view bbox.'topRight'
- anchor at the top-right corner of view bbox.
If an anchor property is not provided, the defaultAnchor
paper option is used instead. The anchors.center
anchor is used by default. You can change the default anchor like this:
paper.options.defaultAnchor = {
name: 'midSide',
args: {
rotate: true,
padding: 20
}
};
In case if the end shape is a link, JointJS looks at linkAnchor
property instead.
Custom anchor
New anchor functions can be defined in the anchors
namespace (e.g. anchors.myAnchor
) or passed directly as a function to the anchor
property of link source/target (or to the defaultAnchor
option of a paper).
In either case, the anchor function must return the anchor as a Point. The function is expected to have the following signature:
(endView: dia.ElementView, endMagnet: SVGElement, anchorReference: g.Point, args: object) => g.Point:
endView | dia.ElementView | The CellView to which we are connecting. The Element model can be accessed as endView.model ; this may be useful for writing conditional logic based on element attributes. |
---|---|---|
endMagnet | SVGElement | The SVGElement in our page that contains the magnet (element/subelement/port) to which we are connecting. |
anchorReference | g.Point | A reference to another component of the link path that may be necessary to find this anchor point. If we are calling this method for a source anchor, it is the first vertex, or if there are no vertices the target anchor. If we are calling this method for a target anchor, it is the last vertex, or if there are no vertices the source anchor... |
SVGElement | ...if the anchor in question does not exist (yet), it is that link end's magnet. (The built-in methods usually use this element's center point as reference.) | |
args | object | An object with additional optional arguments passed to the anchor method by the user when it was called (the args property). |
Link anchor
If the link end (source/target) is a Link (not an Element or a Link subelement/magnet), the precise position of the link end's anchor may be specified by the linkAnchor
property. If a link anchor method is used, connectionPoints
are not applied.
A link end link anchor is a point on another link that this link's path wants to connect to at that source/target end. Alongside link vertices, source and target anchors determine the basic link path.
The link anchor functions reference the end link to find the anchor point - e.g. a point at a given ratio from the start, or the closest point. Several pre-made link anchors are provided inside the JointJS library in the linkAnchors
namespace.
link.source(link2, {
linkAnchor: {
name: 'connectionRatio',
args: {
ratio: 0.25
}
}
});
There are several built-in linkAnchor functions in JointJS:
'connectionRatio'
- default link anchor at specified ratio at reference link.'connectionLength'
- link anchor at specified length along reference link.'connectionPerpendicular'
- link anchor that ensures an orthogonal route to the reference link.'connectionclosest'
- link anchor at the point on reference link that is closest to the last vertex on link path.
If a link anchor function is not provided, the defaultLinkAnchor
paper option is used instead. The linkAnchors.connectionRatio
link anchor with a value of 0.5
is used by default (i.e. the anchor is placed at the midpoint of a Link by default). You can change the default link anchor in the following manner:
paper.options.defaultLinkAnchor = {
name: 'connectionLength',
args: {
length: 20
}
};
If the reference object is an Element, JointJS looks at anchor
property instead.
Custom link anchor
New link anchor functions can be defined in the linkAnchors
namespace (e.g. linkAnchors.myLinkAnchor
) or passed directly as a function to the linkAnchor
property of link source/target (or to the defaultLinkAnchor
option of a paper).
In either case, the link anchor function must return the link anchor as a Point. The function is expected to have the following signature:
(endView: dia.LinkView, endMagnet: null, anchorReference: g.Point, args: object) => g.Point;
endView | dia.LinkView | The LinkView to which we are connecting. The Link model can be accessed as endView.model ; this may be useful for writing conditional logic based on link attributes. |
---|---|---|
endMagnet | null | (Not used) This argument is only present to ensure that the signature of custom linkAnchor methods is the same as the signature of a custom anchor . |
anchorReference | g.Point | A reference to another component of the link path that may be necessary to find this anchor point. If we are calling this method for a source anchor, it is the first vertex; if there are no vertices, it is the target anchor. If we are calling this method for a target anchor, it is the last vertex; if there are no vertices, it is the source anchor... |
SVGElement | ...if the anchor in question does not exist (yet), it is that link end's magnet. (The built-in methods usually use this element's center point as reference.) | |
args | object | An object with additional optional arguments passed to the link anchor method by the user when it was called (the args property). |
Connection point
The link end's connection point may be specified by the connectionPoint
property. Every link has two connection points; one at the source end and one at the target end.
A link connection point is the point at which the link path actually ends at, taking the end element into account. This point will always lie on the link path (the line connecting link anchors and vertices together, in order).
The connection points are found by considering intersections between the link path and a desired feature of the end element (e.g. bounding box, shape boundary, anchor). Although connection points are not capable of being offset off the link path (anchors should be used to modify the link path if this is required), they can be offset along the path - e.g. to form a gap between the element and the actual link. Several pre-made connection points are provided in the JointJS library in the connectionPoints
namespace. However, the functions always only have access to a single path segment; the source connectionPoint is found by investigating the first segment (i.e. source anchor - first vertex, or source anchor - target anchor if there are no vertices), while the target connectionPoint is found by investigating the last segment (i.e. last vertex - target anchor, or source anchor - target anchor). This has consequences if the investigated path segment is entirely contained within the end element.
link.source(rect, {
connectionPoint: {
name: 'boundary',
args: {
offset: 5
}
}
});
There are four built-in connection point functions in JointJS:
'anchor'
- connection point at anchor.'bbox'
- default connection point at bbox boundary.'boundary'
- connection point at actual shape boundary.'rectangle'
- connection point at unrotated bbox boundary.
If a connection point property is not provided, the defaultConnectionPoint
paper option is used instead. The connectionPoints.bbox
connection point is used by default. Here is the example of changing the default connection point:
paper.options.defaultConnectionPoint = {
name: 'boundary',
args: {
sticky: true
}
};
Custom connection point
New connection point function can be defined in the connectionPoints
namespace (e.g. connectionPoints.myConnectionPoint
) or passed directly as a function to the connectionPoint
property of link source/target (or to the defaultConnectionPoint
option of a paper).
In either case, the connection point function must return the connection point as a Point. The function is expected to have the following signature:
(endPathSegmentLine: g.Line, endView: dia.ElementView, endMagnet: SVGElement, args: object) => g.Point;
endPathSegmentLine | g.Line | The link path segment at which we are finding the connection point. If we are calling this method for a source connection point, it is the first segment (source anchor - first vertex, or source anchor - target anchor). If we are calling this method for a target connection point, it is the last segment (last vertex - target anchor, or source anchor - target anchor). |
---|---|---|
endView | dia.ElementView | The ElementView to which we are connecting. The Element model can be accessed as endView.model ; this may be useful for writing conditional logic based on element attributes. |
endMagnet | SVGElement | The SVGElement in our page that contains the magnet (element/subelement/port) to which we are connecting. |
args | object | An object with additional optional arguments passed to the connection point method by the user when it was called (the args property). |
Connection strategy
Connection strategies come into play when the user modifies the position of link endpoints. There are two situations in which this is relevant:
- When the user drags a link endpoint and connects it to an element or its port. The connection strategy determines the end anchor after the user is finished dragging the link endpoint. (Note that any individual
anchor
property that might have been assigned on the dragged link endpoint will be overridden by the connection strategy). - When a user creates a link, for example by clicking a port. The connection strategy determines the new link's source anchor.
There are three built-in connection strategies in JointJS:
useDefaults
- default strategy; ignore user pointer and use default anchor and connectionPoint functions.pinAbsolute
- use user pointer coordinates to position anchor absolutely within end element; use default connectionPoint.pinRelative
- use user pointer coordinates to position anchor relatively within end element; use default connectionPoint.
Both the anchor
and connectionPoint
properties are rewritten in response to user interaction. None of the built-in connection strategies preserve the originally assigned anchor and connection point functions. To assign precisely what you need as the anchor and connection point functions, you may need to define your own custom connection strategy.
Connection strategies are not assigned on a link-by-link basis. They are set with the connectionStrategy
option on a paper.
The default connection strategy is specified as null
in paper settings, which is equivalent to connectionStrategies.useDefaults
.
Built-in connection strategies are specified by reference to their their name in the connectionStrategies
namespace. Example:
import { connectionStrategies } from '@joint/core';
paper.options.connectionStrategy = connectionStrategies.pinAbsolute;
If a connection strategy is not provided, the connectionStrategies.useDefaults
strategy is used by default.
Custom connection strategy
New connection strategies can be defined in the connectionStrategies
namespace (e.g. connectionStrategies.myConnectionStrategy
) or passed directly as a function to the connectionStrategy
option of a paper.
In either case, the connection strategy function must return an end definition (i.e. an object in the format supplied to the link.source()
and link.target()
functions). The function is expected to have the following signature:
(endDefinition: object, endView: dia.ElementView, endMagnet: SVGElement, coords: g.Point) => object;
endDefinition | object | An end definition; the output of the appropriate end function (link.source() or link.target() ). An object containing at least an id of the Element to which we are connecting. This object is expected to be changed by this function and then sent as the return value. |
---|---|---|
endView | dia.ElementView | The ElementView to which we are connecting. The Element model can be accessed as endView.model ; this may be useful for writing conditional logic based on element attributes. |
endMagnet | SVGElement | The SVGElement in our page that contains the magnet (element/subelement/port) to which we are connecting. |
coords | g.Point | A Point object recording the x-y coordinates of the user pointer when the connection strategy was invoked. |
Custom connection strategies may be enormously useful for your users. Here we provide some examples of custom functionality.
Connecting to Ancestors
If your diagram makes heavy use of nested elements, it may be useful to always connect links to a top-level ancestor element (instead of the element on which the arrowhead was actually dropped by user interaction):
import { connectionStrategies } from '@joint/core';
connectionStrategies.topAncestor = (end, endView) => {
const ancestors = endView.model.getAncestors();
const numAncestors = ancestors.length;
end = numAncestors ? ancestors[numAncestors - 1] : end;
return end;
}
paper.options.connectionStrategy = connectionStrategies.topAncestor;
Connecting to Ports
If your diagram uses ports, you usually do not want links to be able to connect anywhere else. The solution is similar to the one above:
import { connectionStrategies } from '@joint/core';
connectionStrategies.firstPort = (end, endView) =>{
const ports = endView.model.getPorts();
const numPorts = ports.length;
end = numPorts ? { id: end.id, port: ports[0].id } : end;
return end;
}
paper.options.connectionStrategy = connectionStrategies.firstPort;
Replicating Built-in Anchor Functions
Furthermore, it is very easy to replicate the built-in anchor functions for connection strategy scenarios - simply apply the anchor function to the received end
parameter:
import { connectionStrategies } from '@joint/core';
connectionStrategy.midSide = (end) => {
end.anchor = {
name: 'midSide',
args: {
rotate: true
}
};
return end;
}
paper.options.connectionStrategy = connectionStrategy.midSide;
Replicating Built-in Connection Point Functions
What if we needed to replicate a built-in connection point function instead? We use the same idea as in the previous example:
import { connectionStrategies } from '@joint/core';
connectionStrategy.boundary = (end) => {
end.connectionPoint = {
name: 'boundary',
args: {
offset: 5
}
};
return end;
}
paper.options.connectionStrategy = connectionStrategy.boundary;
Of course, it is also possible to combine both of the examples and assign an anchor
as well as connectionPoint
to the end
parameter of the modified link.
Vertices
The vertices
attribute is an array of user-defined points for the link to pass through. Alongside the source and target anchors
, the link vertices determine the basic link path. This skeleton path is then used for determining the link route using router. The vertices can be accessed with the link.vertices()
function and related functions.
link.vertices();
link.vertices([{ x: 100, y: 120 }, { x: 150, y: 60 }]);
Router
Routers take an array of link vertices and transform them into an array of route points that the link should go through. The difference between vertices
and the route is that the vertices are user-defined while the route is computed. The route inserts additional private vertices to complement user vertices as necessary (e.g. to make sure the route is orthogonal). This route is then used for generating the connection SVG path commands using connector. The router
attribute of a link can be accessed with the link.router()
function.
A collection of pre-made routers is provided inside the JointJS library in the routers
namespace. This includes "smart routers" that are able to automatically avoid obstacles (elements) in their way.
There are five built-in routers:
'manhattan'
- smart orthogonal router.'metro'
- smart octolinear router.'normal'
- default simple router.'orthogonal'
- basic orthogonal router.'rightAngle'
- orthogonal router that goes in a given direction.
link.router('manhattan', {
excludeEnds: ['source'],
excludeTypes: ['myNamespace.MyCommentElement'],
startDirections: ['top'],
endDirections: ['bottom']
});
The 'manhattan'
and 'metro'
routers are our smart routers; they automatically avoid obstacles (elements) in their way.
The 'orthogonal'
, 'manhattan'
and 'rightAngle'
routers generate routes consisting exclusively of vertical and horizontal segments. The 'metro'
router generates routes consisting of orthogonal and diagonal segments.
Modular architecture of JointJS allows mixing-and-matching routers with connectors as desired; for example, a link may be specified to use the 'jumpover'
connector on top of the 'manhattan'
router:
const link = new shapes.standard.Link();
link.source(rect);
link.target(rect2);
link.router('manhattan');
link.connector('jumpover');
If a router is not provided, the defaultRouter
paper option is used instead. The routers.normal
router is used by default. You can change the default router like this:
paper.options.defaultRouter = {
name: 'orthogonal',
args: {
padding: 10
}
};
Custom router
New routers can be defined in the routers
namespace (e.g. routers.myRouter
) or passed directly as a function to the router
property of a link (or to the defaultRouter
option of a paper).
In either case, the router function must return an array of route points that the link should go through (not including the start/end connection points). The function is expected to have the following signature:
(vertices: Array<g.Point>, args: object, linkView: dia.LinkView) => Array<g.Point>;
vertices | Array<g.Point> | The vertices of the route. |
---|---|---|
args | object | An object with additional optional arguments passed to the router method by the user when it was called (the args property). |
linkView | dia.LinkView | The LinkView of the connection. The Link model can be accessed as the model property; this may be useful for writing conditional logic based on link attributes. |
Example of a router defined in the routers
namespace:
import { routers, g, shapes } from '@joint/core';
routers.randomWalk = (vertices, args, linkView) => {
const NUM_BOUNCES = args.numBounces || 20;
vertices = joint.util.toArray(vertices).map(g.Point);
for (let i = 0; i < NUM_BOUNCES; i++) {
const sourceCorner = linkView.sourceBBox.center();
const targetCorner = linkView.targetBBox.center();
const randomPoint = g.Point.random(sourceCorner.x, targetCorner.x, sourceCorner.y, targetCorner.y);
vertices.push(randomPoint)
}
return vertices;
}
const link = new shapes.standard.Link();
link.source(source);
link.target(target);
link.router('randomWalk', {
numBounces: 10
});
An example of a router passed as a function is provided below. Notice that this approach does not enable passing custom args
nor can it be serialized with the graph.toJSON()
function.
import { util, g, shapes } from '@joint/core';
const link = new shapes.standard.Link();
link.source(source);
link.target(target);
link.router((vertices, args, linkView) => {
const NUM_BOUNCES = 20;
vertices = util.toArray(vertices).map(g.Point);
for (let i = 0; i < NUM_BOUNCES; i++) {
const sourceCorner = linkView.sourceBBox.center();
const targetCorner = linkView.targetBBox.center();
const randomPoint = g.Point.random(sourceCorner.x, targetCorner.x, sourceCorner.y, targetCorner.y);
vertices.push(randomPoint)
}
return vertices;
});
Connector
Connectors take an array of link route points and generate SVG path commands so that the link can be rendered. The connector
attribute of a link can be accessed with the link.connector()
function.
A collection of pre-made connectors is provided inside the JointJS library in the connectors
namespace.
There are six built-in connectors in JointJS:
'straight'
- connector with straight lines and different ways to handle corners (point, rounded, bevelled, gap).'jumpover'
- connector with angled straight lines and bridges over link intersections.'normal'
- (deprecated) default connector with angled straight lines.'rounded'
- (deprecated) connector with straight lines and rounded edges.'smooth'
- connector interpolated as a bezier curve.'curve'
- connector interpolating as a curve defined by ending tangents.
link.connector('straight', {
cornerType: 'cubic',
cornerRadius: 20
});
The default connector is 'normal'
; this can be changed with the defaultConnector
paper option. Example:
paper.options.defaultConnector = {
name: 'straight',
args: {
cornerType: 'line',
cornerRadius: 20
}
}
Custom connector
New connectors can be defined in the connectors
namespace (e.g. connectors.myConnector
) or passed directly as a function to the connector
property of a link (or to the defaultConnector
paper option).
In either case, the connector function must return a g.Path representing the SVG path data that will be used to render the link. The function is expected to have the following signature:
(sourcePoint: g.Point, targetPoint: g.Point, routePoints: Array<g.Point>, args: object) => g.Path;
sourcePoint | g.Point | The source connection point. |
---|---|---|
targetPoint | g.Point | The target connection point. |
routePoints | Array<g.Point> | The points of the route, as returned by the router in use. |
args | object | An object with additional optional arguments passed to the connector method by the user when it was called (the args property). |
Example of a connector defined in the connectors
namespace:
import { connectors, g, shapes } from '@joint/core';
connectors.wobble = (sourcePoint, targetPoint, vertices, args) => {
const SPREAD = args.spread || 20;
const points = vertices.concat(targetPoint)
const prev = sourcePoint;
const path = new g.Path(g.Path.createSegment('M', prev));
const n = points.length;
for (let i = 0; i < n; i++) {
const next = points[i];
const distance = prev.distance(next);
let d = SPREAD;
while (d < distance) {
const current = prev.clone().move(next, -d);
current.offset(
Math.floor(7 * Math.random()) - 3,
Math.floor(7 * Math.random()) - 3
);
path.appendSegment(g.Path.createSegment('L', current));
d += SPREAD;
}
path.appendSegment(g.Path.createSegment('L', next));
prev = next;
}
return path;
}
const link = new shapes.standard.Link();
link.source(source);
link.target(target);
link.connector('wobble', {
spread: 10
});
An example of a connector passed as a function is provided below. Notice that this approach does not enable passing custom args
nor can it be serialized with the graph.toJSON()
function.
import { g, shapes } from '@joint/core';
const link = new shapes.standard.Link();
link.source(source);
link.target(target);
link.connector((sourcePoint, targetPoint, vertices, args) => {
const SPREAD = 20;
const points = vertices.concat(targetPoint)
const prev = sourcePoint;
const path = new g.Path(g.Path.createSegment('M', prev));
const n = points.length;
for (let i = 0; i < n; i++) {
const next = points[i];
const distance = prev.distance(next);
let d = SPREAD;
while (d < distance) {
const current = prev.clone().move(next, -d);
current.offset(
Math.floor(7 * Math.random()) - 3,
Math.floor(7 * Math.random()) - 3
);
path.appendSegment(g.Path.createSegment('L', current));
d += SPREAD;
}
path.appendSegment(g.Path.createSegment('L', next));
prev = next;
}
return path;
});