36

Given this SVG:

<svg xmlns="http://www.w3.org/2000/svg">
  <rect width="50" height="50"
        transform="translate(75,75) rotate(45) translate(-25,-25)" />
  <script>
    var bb = document.querySelector('rect').getBBox();
    console.log([bb.x,bb,y,bb.width,bb.height]);
  </script>
</svg>​

The resulting output is [0, 0, 50, 50].
The desired result is [39.645,39.645,70.711,70.711].

Visual Version: http://jsfiddle.net/2wpju/7/

What's the simplest, most efficient way to calculate the bounding box of an element with respect to its parent element?


Below is the best answer I've come up with so far, but there are two problems:

  1. It seems like a lot of work for what might be handled better, and
  2. As seen in this demo it's not necessarily the minimum bounding box (notice the upper circle), since the axis-aligned bounding box of a rotated axis-aligned bounding box always increases the size.

 

// Calculate the bounding box of an element with respect to its parent element
function transformedBoundingBox(el){
  var bb  = el.getBBox(),
      svg = el.ownerSVGElement,
      m   = el.getTransformToElement(el.parentNode);

  // Create an array of all four points for the original bounding box
  var pts = [
    svg.createSVGPoint(), svg.createSVGPoint(),
    svg.createSVGPoint(), svg.createSVGPoint()
  ];
  pts[0].x=bb.x;          pts[0].y=bb.y;
  pts[1].x=bb.x+bb.width; pts[1].y=bb.y;
  pts[2].x=bb.x+bb.width; pts[2].y=bb.y+bb.height;
  pts[3].x=bb.x;          pts[3].y=bb.y+bb.height;

  // Transform each into the space of the parent,
  // and calculate the min/max points from that.    
  var xMin=Infinity,xMax=-Infinity,yMin=Infinity,yMax=-Infinity;
  pts.forEach(function(pt){
    pt = pt.matrixTransform(m);
    xMin = Math.min(xMin,pt.x);
    xMax = Math.max(xMax,pt.x);
    yMin = Math.min(yMin,pt.y);
    yMax = Math.max(yMax,pt.y);
  });

  // Update the bounding box with the new values
  bb.x = xMin; bb.width  = xMax-xMin;
  bb.y = yMin; bb.height = yMax-yMin;
  return bb;
}
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • Interestingly, the "too big" example of the rotated circle is same as the result of calling `getBoundingClientRect()` for the element, on all major browsers. Apparently they internally do the same thing (find the local bounding rect and then transform those points into screen space). – Phrogz May 16 '12 at 21:07
  • Previous comment [does not apply to Firefox](http://stackoverflow.com/questions/10628851/what-should-getboundingclientrect-on-a-transformed-svg-element-return). – Phrogz May 25 '12 at 21:39
  • cool, works as expected – user85155 Mar 25 '13 at 15:58
  • This has helped me a lot, as I've been struggling with matrix transformations with svg and bounding. I was looking for a simpler solution as well that I could get my head around easier, however this looked to work straight away. – Ian Oct 29 '13 at 16:59
  • I think you meant the desired result to be: [39.64466,39.64466,110.35533,110.35533] – Diego Oct 31 '13 at 17:13
  • 7
    This is a great little utility function, but unfortunately Google Chrome removed the `getTransformToElement()` method recently. The workaround is to polyfill `SVGElement` : if (typeof SVGElement.prototype.getTransformToElement === 'undefined') { SVGElement.prototype.getTransformToElement = function(toElement) { return toElement.getScreenCTM().inverse().multiply(this.getScreenCT‌​M()); }; } – cvkline Sep 25 '16 at 05:58
  • your own answer made my day: i need to do the same but can't rely on getBoundingClientRect due to svg resizing. event if verbose, that's what i needed. thanks!!! – lbrutti Apr 01 '21 at 15:49

6 Answers6

3

All you need to use is this simple Javascript function:

object.getBoundingClientRect();

It is supported in:

Chrome  Firefox (Gecko)   Internet Explorer Opera   Safari
1.0           3.0 (1.9)   4.0               (Yes)   4.0

Look here for more about it. Link

Progo
  • 3,452
  • 5
  • 27
  • 44
  • 5
    That will apply all the transforms on every parent element, not just the one on the element itself. – Robert Longson Dec 31 '13 at 13:54
  • 4
    This works, but beware that it's calculated in screen space, not in page space. Thus, for SVGs larger than the window, getBoundingClientRect will give different results based on scroll offset. – Codesmith Nov 14 '17 at 18:20
3

If you do not want to deal with math, a good workaround is to wrap the SVG element in a <g> group and then call getBBox() on the new group.

Here is pseudo code for it


function getBBoxTakingTransformIntoAccount(element) {
        let group = wrapInGroup(element)
        let bBox = group.getBBox()
        unwrapChild(group) // Optionally, you can unwrap the child node
        return bBox
    }

function wrapInGroup(element) {
        let group = document.createElementNS("http://www.w3.org/2000/svg", "g")
        element.parentNode.appendChild(group)
        group.appendChild(element)
        return group
    }

function unwrapChild(element) {
        let child = element.children[0]
        element.parentNode.appendChild(child)
        element.remove()
    }

Islam Abdalla
  • 1,464
  • 1
  • 13
  • 12
2

That seems to be the current method, unless/until getBBox() considers the transform on the object as directly defining the bounding box, even though SVG 1.1 SE defines:

SVGRect getBBox() Returns the tight bounding box in current user space (i.e., after application of the ‘transform’ attribute, if any) on the geometry of all contained graphics elements, exclusive of stroking, clipping, masking and filter effects). Note that getBBox must return the actual bounding box at the time the method was called, even in case the element has not yet been rendered.

I guess its all about how you define 'current user space'

Minok
  • 522
  • 4
  • 9
2

This is an answer that accounts for rotated ellipses. The transformation method works OK for rectangles but on ellipses returns a bounding box considerably larger than the object if it is rotated.

The simpler answer of using the builtin JavaScript function isn't very useful to me because it would still have to be translated back into the drawing space.

This code doesn't handle skew or scale for ellipses (but does for any other object), but in my program at http://nwlink.com/~geoffrey/svgBuild I intend to handle skew and scale by drawing an entirely new ellipse with the skew or scale applied.

The formula for the ellipses came from here: How do you calculate the axis-aligned bounding box of an ellipse? much thanks for the help there.

el is a d3.js element, not the node.

function toNumberIfNumeric(x) {
    var result = parseFloat(x);
    return isNaN(result) ? x : result;
}

function getAttrs(el, attributes) {
    var result = {};
    attributes.forEach(function (attr) {
        result[attr] = toNumberIfNumeric(el.attr(attr));
    });
    return result;
}    

function getTransformedBbox(el, referenceNode) {

    var node = el.node();

    switch (node.nodeName) {
        case "ellipse":

            var rotate = getTransform(el, "rotate");
            if (rotate.angle === 0) {
                return node.getBBox();
            }
            var attr = getAttrs(el, ["cx", "cy", "rx", "ry"]);
            var radians = rotate.angle / 180 * Math.PI;
            var radians90 = radians + Math.PI / 2;

            var ux = attr.rx * Math.cos(radians);
            var uy = attr.rx * Math.sin(radians);
            var vx = attr.ry * Math.cos(radians90);
            var vy = attr.ry * Math.sin(radians90);

            var halfWidth = Math.sqrt(ux * ux + vx * vx);
            var halfHeight = Math.sqrt(uy * uy + vy * vy);

            return {
                x: attr.cx - halfWidth,
                y: attr.cy - halfHeight,
                width: 2 * halfWidth,
                height: 2 * halfHeight
            };

        default:
            var bb = node.getBBox(),
                svg = node.ownerSVGElement,
                m = node.getTransformToElement(referenceNode);

            // Create an array of all four points for the original bounding box
            var pts = [
                svg.createSVGPoint(), svg.createSVGPoint(),
                svg.createSVGPoint(), svg.createSVGPoint()
            ];
            pts[0].x = bb.x;
            pts[0].y = bb.y;
            pts[1].x = bb.x + bb.width;
            pts[1].y = bb.y;
            pts[2].x = bb.x + bb.width;
            pts[2].y = bb.y + bb.height;
            pts[3].x = bb.x;
            pts[3].y = bb.y + bb.height;

            // Transform each into the space of the parent,
            // and calculate the min/max points from that.    
            var xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity;
            pts.forEach(function (pt) {
                pt = pt.matrixTransform(m);
                xMin = Math.min(xMin, pt.x);
                xMax = Math.max(xMax, pt.x);
                yMin = Math.min(yMin, pt.y);
                yMax = Math.max(yMax, pt.y);
            });

            // Update the bounding box with the new values
            bb.x = xMin;
            bb.width = xMax - xMin;
            bb.y = yMin;
            bb.height = yMax - yMin;
            return bb;
    }
}
Community
  • 1
  • 1
GeeWhizBang
  • 699
  • 4
  • 27
2

Necromancing.

The bounding-box coordinates you need are always relative to the transform of the element you want to use the coordinates in.

e.g. you have a path in a

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 577">
  <g id="test">
    <path id="e123" d="m906.76 442.4h51.77v39.59h-51.77z" fill="#B3B3B3" fill-rule="evenodd" transform="matrix(1,0,0,0.999749,0,-75.2041)"/>
  </g>
</svg>

Now you want to add another group

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 577">
  <g id="test">
    <path id="e123" d="m906.76 442.4h51.77v39.59h-51.77z" fill="#B3B3B3" fill-rule="evenodd" transform="matrix(1,0,0,0.999749,0,-75.2041)"/>
  </g>

  <g id="e456" data-group-id="AP">
    <rect class="BG" fill="yellow" x="906.760009765625" y="390.5399169921875" width="15.96875" height="16.125"/>
    <text id="foobar" fill="#000" font-size="1.5mm" data-lines-count="4" x="906.760009765625" y="390.5399169921875">
      <tspan x="906.760009765625" dy="1.2em">.</tspan>
      <tspan x="906.760009765625" dy="1.2em">ABC123</tspan>
      <tspan x="906.760009765625" dy="1.2em">Test</tspan>
      <tspan x="906.760009765625" dy="1.2em">123</tspan>
    </text>
  </g>


</svg>

And now you want to put the text-element coordinates and the rectangle coordinates in that new group to the bounding-box of element with id "e123" (a rectangle).

So you take

document.getElementById("e123").getBBox();

This bounding box takes into account the transform you have in e123, aka after application of the ‘transform’ attribute. And if you take these coordinates outside e123 (imagine e123 was a group instead of a path), then the coordinates will be off by that transform.

Therefore, you need to take the bounding box of your element in question (fromSpace), and transform them to coordinates appropriate for your target element, e.g. "e456".

You take the coordinates of the bounding-box of your fromSpace, create two points (top-Left, bottom-right), transform them into the target space, and from these two points, compute the new bounding-box (note that it doesn't work when you just set x2 to bbox.width and y2 to bbox.height - you need the other point, and then recompute width and height after applying the matrix to each point).

In code:

if (!SVGElement.prototype.getTransformToElement)
{
    SVGElement.prototype.getTransformToElement = function (elem)
    {
        return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
    };
}


function boundingBoxRelativeToElement(fromSpace, toSpace, bbox)
{
  bbox = bbox || fromSpace.getBBox();
  
  // var m = fromSpace.transform.baseVal[0].matrix;
  var m = fromSpace.getTransformToElement(toSpace);
    
  var bbC = document.documentElement.createSVGPoint(); bbC.x = bbox.x; bbC.y = bbox.y;
  var bbD = document.documentElement.createSVGPoint(); bbD.x = bbox.x + bbox.width; bbD.y = bbox.y + bbox.height;
  // console.log("bbC", bbC, "bbD", bbD);
  bbC = bbC.matrixTransform(m);
  bbD = bbD.matrixTransform(m);
    
  var bb = { x: bbC.x, y: bbC.y, width: Math.abs(bbD.x - bbC.x), height: Math.abs(bbD.y - bbC.y)};
  console.log("bb", bb);
  return bb;
}

So you use it like this:

var elFrom = document.getElementById("e123");
var elTo = document.getElementById("e456");
var bbox = boundingBoxRelativeToElement(elFrom, elTo);

And if you just want the boundingBox in parentElement-coordinates, this would be:

var elFrom = document.getElementById("e123");
var elTo = elFrom.parentElement;
var bbox = boundingBoxRelativeToElement(elFrom, elTo);

aka

var elFrom = document.getElementById("e123");
var bbox = boundingBoxRelativeToElement(elFrom, elFrom.parentElement);

or as a helper function

function boundingBoxRelativeToParent(el)
{
    var bbox = boundingBoxRelativeToElement(el, el.parentElement);
    return bbox;
}

Which is then easy to use:

var bbox = boundingBoxRelativeToParent(document.getElementById("e123"));

(note that this assumes that parentElement is NOT null)

Stefan Steiger
  • 78,642
  • 66
  • 377
  • 442
  • ```return { x: Math.min(bbC.x, bbD.x), y: Math.min(bbC.y, bbD.y), width: Math.abs(bbD.x - bbC.x), height: Math.abs(bbD.y - bbC.y) };``` – loop Jul 11 '21 at 13:20
1

One way is to make a raphael set and get bb of it. Or wrap all elements inside g and get bb of g, i assume that g should be fastest.

Timo Kähkönen
  • 11,962
  • 9
  • 71
  • 112