Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3 - Create Dynamic "Border" Rectangle around SVG group

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?

like image 429
agilgur5 Avatar asked Aug 01 '14 18:08

agilgur5


3 Answers

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.

like image 158
Erik Dahlström Avatar answered Oct 13 '22 03:10

Erik Dahlström


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.

like image 39
jshanley Avatar answered Oct 13 '22 01:10

jshanley


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>
like image 22
agilgur5 Avatar answered Oct 13 '22 02:10

agilgur5