Skip to main content

Relative dimensions

Relative dimensions with calc function

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. 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.

ref attributes

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. Stacks with the native x/y attribute.
  • refCx and refCy - sets the coordinates of the circle/ellipse center. Percentages are interpreted relative to model bbox. Can be used alongside 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.

Introduction to calc

The calc() function lets you perform calculations when specifying SVG attributes values.

Syntax:

The calc() function takes a simple expression as its parameter, with the expression's result used as the value. The expression can be any simple expression in one of the following forms:

  • Variable
'calc(w)'
  • Variable Addition or Subtraction
'calc(w + 5)'
'calc(h - 10)'
  • Multiplication Variable
'calc(2 * w)'
'calc(0.5 * h)'
  • Variable Division
'calc(w / 2)'
'calc(h / 3)'
  • Variable Division
'calc(w / 2)'
'calc(h / 3)'
  • Multiplication Variable Addition or Subtraction
'calc(2 * w + 5)'
'calc(0.5 * h - 10)'
  • Variable Division Addition or Subtraction
'calc(w / 2 + 5)'
'calc(h / 3 - 10)'

Where:

  • Variable is a symbol representing a value that can change, when the model attributes change (size, attrs).
variablenamedescription
wwidthThe current width of the model (model.prop('size/width')). The value can be bound to an SVGElement's size instead by using ref attribute.
hheightThe current height of the model (model.prop('size/height')). The value can be bound to an SVGElement's size instead by using ref attribute.
xxThe current x coordinate of the SVGElement in the element's coordinate system. If the attribute is not bound to a specific SVGElement with ref attribute, the value of x is always zero.
yyThe current y coordinate of the SVGElement in the element's coordinate system. If the attribute is not bound to a specific SVGElement with ref attribute, the value of y is always zero.
sshortestThe shortest side of the rectangle. The minimum of width and height.
llongestThe longest side of the rectangle. The maximum of width and height.
ddiagonalThe length of the diagonal of the rectangle of size width and height.
  • Multiplication is an optional floating number factor of the variable. It's a number followed by the * symbol.
1.5 *
  • Division is an optional floating number divisor of the variable. It's the / symbol followed by a number.
/ 2
  • Addition or Subtraction is an optional floating number added or subtracted from the variable. It's a + or - symbol followed by a number.
+ 5
Notes:
  • Expression is case-sensitive.
  • The +, - and * operators do not require whitespace.
  • No extra parentheses are allowed.
  • It is permitted to nest calc() functions, in which case the inner ones are evaluated first. e.g.
'M 0 0 H calc(w - calc(h))'

It can be used with the following attributes:

Calc examples:

el.resize(200, 100); // dia.Element

// <rect joint-selector="myRect" width="200" height="100" rx="20" ry="10" />
el.attr('myRect', {
width: 'calc(w)',
height: 'calc(h)',
rx: 'calc(0.1*w)',
ry: 'calc(0.1*h)'
});

// <image joint-selector="myImage" x="105" y="55" />
el.attr('myImage/x', 'calc(0.5*w+5)');
el.attr('myImage/y', 'calc(0.5*h+5)');

// <path joint-selector="myPath" d="M 10 50 190 50" />
el.attr('myPath/d', 'M 10 calc(0.5*h) calc(w-10) calc(0.5*h)')

// <polygon joint-selector="myPolygon" points="0,0 200,0 200,100 0,100" />
el.attr('myPolygon/d', '0,0 calc(w),0 calc(w),calc(h) 0,calc(h)');

// Resize the rectangle to match the text size with extra 5 pixels of padding
// <rect joint-selector="myTextBackground" />
// <text joint-selector="myText" >Some text</text>
el.attr('myTextBackground', {
ref: 'myText',
x: 'calc(x - 5)',
y: 'calc(y - 5)'
width: 'calc(w + 10)',
height: 'calc(h + 10)',
});

Example

Let's see relative sizing with calc() in action. We define a custom element type named CustomElement as a subtype of 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:

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

const CustomElement = 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: util.svg`
<ellipse @selector="e"/>
<rect @selector="r"/>
<circle @selector="c"/>
<rect @selector="outline"/>
`
});

const 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)'
}
});

Here is the example in action:

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:

  • 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 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:

const CustomTextElement = 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: util.svg`
<ellipse @selector="e"/>
<rect @selector="r"/>
<circle @selector="c"/>
<text @selector="label"/>
<rect @selector="outline"/>
`
});

const 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
}
});

Here is the example in action:

Frequently asked questions

How can I automatically adjust an element's height and width to accommodate the text provided by the user?

You can define a custom element type with a <text> subelement to which other subelements refer via the ref attribute - see above.

Learn more...

You can explore more examples of this feature on our demo page - https://www.jointjs.com/demos?feature=Content-driven+shapes.