I have an SVG group with a rect inside of it, and would like the rect to act as a border for the group...
<g>
<rect></rect>
</g>
but the group is dynamic and its content changes. I am attempting to resize the rect in my update function as such
.attr("x", function(d) { return this.parentNode.getBBox().x })
.attr("y", function(d) { return this.parentNode.getBBox().y })
.attr("width", function(d) { return this.parentNode.getBBox().width })
.attr("height", function(d) { return this.parentNode.getBBox().height })
But what seems to happen is that it expands relatively fine, but then cannot shrink properly since the group's bounding box width is now the same as the expanded rect's width (the rect's width is the group's width, but the group's width is now the rect's width).
Is there any way to get a rectangle inside an SVG group to properly resize and act as a border?
There's more than one way to solve this.
Use the outline
property (2014-08-05 status: works in Chrome and Opera)
<svg xmlns="http://www.w3.org/2000/svg" width="500px" height="500px">
<g style="outline: thick solid black; outline-offset: 10px;">
<circle cx="50" cy="60" r="20" fill="yellow"/>
<rect x="80" y="80" width="200" height="100" fill="blue"/>
</g>
</svg>
See live example.
Use a filter
to generate the border (2014-08-05 status: works in Firefox, but Chrome/Opera has a bug on feMorphology, but it should be possible to work around that by using other filter primitives).
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
<defs>
<filter id="border" x="-5%" y="-5%" width="110%" height="110%">
<feFlood flood-color="black" result="outer"/>
<feMorphology operator="erode" radius="2" in="outer" result="inner"/>
<feComposite in="inner" in2="outer" operator="xor"/>
<feComposite in2="SourceGraphic"/>
</filter>
</defs>
<g filter="url(#border)">
<circle cx="50" cy="60" r="20" fill="yellow"/>
<rect x="80" y="80" width="200" height="100" fill="blue"/>
</g>
</svg>
See live example.
Both of the above will automatically update to whatever size the group has, without the need for DOM modifications.
Yes, you can find the new bounding box by selecting all child elements of the group that are not the bounding rect itself, and then calculating the overall bounding box based on the individual bounding boxes of the children.
Lets say your bounding rect had a class of bounding-rect
, you could do the following:
function updateRect() {
// SELECT ALL CHILD NODES EXCEPT THE BOUNDING RECT
var allChildNodes = theGroup.selectAll(':not(.bounding-rect)')[0]
// `x` AND `y` ARE SIMPLY THE MIN VALUES OF ALL CHILD BBOXES
var x = d3.min(allChildNodes, function(d) {return d.getBBox().x;}),
y = d3.min(allChildNodes, function(d) {return d.getBBox().y;}),
// WIDTH AND HEIGHT REQUIRE A BIT OF CALCULATION
width = d3.max(allChildNodes, function(d) {
var bb = d.getBBox();
return (bb.x + bb.width) - x;
}),
height = d3.max(allChildNodes, function(d) {
var bb = d.getBBox();
return (bb.y + bb.height) - y;
});
// UPDATE THE ATTRS FOR THE RECT
svg.select('.bounding-rect')
.attr('x', x)
.attr('y', y)
.attr('width', width)
.attr('height', height);
}
This would set the x and y values of the overall bounding box to be the minimum x and y values in the childrens' bounding boxes. Then the overall width is calculated by finding the maximum right boundary bb.x + bb.width
and subtracting the overall box's x. The overall height is then calculated in the same way as the width.
HERE is an example of this.
The simplest, cross-browser compatible way is to implement a border is to use a rect
exactly as I did, but place it outside of the group, as mentioned by @Duopixel in his comment. As it is still positioned by the bounding box, it will have the correct width, height, x, and y.
<rect></rect>
<g></g>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With