D3 SVG Move zoom to rect inside <g> having different angle

Viewed 131

I have <svg> with background images and different rectangle on that svg that are in proper position.
My rectangles are grouped by <g> with translation values and each rectangle has specific rotation values. Now I want my SVG to focus (show in center of page) on specific rectangle without disturbing background image and svg translate there.. Main issue is that these rectangles have different angles too. So translate is not working properly....

SVG is like as below.
Can anyone provide me with guidelines?

<svg width="4500" height="3610" style="background: url(&quot;/images/001.jpg&quot;) no-repeat;">   
<g id="228" transform="translate(2001,620)rotate(216) translate(0 -216)">
 <g>
<rect x="0" y="0" height="72" width="36" stroke="black" style="fill: transparent;">
</rect><text x="0" y="36" text-anchor="start" style="fill: black;">228</text>
</g>
 <g>
  <rect x="36" y="0" height="72" width="36" stroke="black" style="fill: transparent;">
  </rect><text x="36" y="36" text-anchor="start" style="fill: black;">229</text>
   </g>
 </g>
 </svg>
1 Answers

This should be easier in SVG, and it might be, but here's my solution:

First, we need to get the bounding box of the rectangle we want to zoom to in it's coordinate space:

// Bounds in local coordinates space:
var bounds = rectElement.getBBox();

Second we get four corners out of this bounding box:

 // Corners of rect in local coordinate space:
 var corners = [
     {x: bounds.x,y: bounds.y},
     {x: bounds.x + bounds.width, y: bounds.y},
     {x: bounds.x + bounds.width, y: bounds.y + bounds.height },
     {x: bounds.x, y: bounds.y + bounds.height }
 ];

Third, we get the transformation matrix between the rectangle and the SVG:

 // relevant transform:
 var t = rectElement.getCTM();

Now, we can convert the corners we have to global SVG units:

  // Convert the points to global SVG coordainte space:
  for(var i = 0; i < corners.length; i++) {
      var point = svg.node().createSVGPoint();
      point.x = corners[i].x;
      point.y = corners[i].y;
      corners[i] = point.matrixTransform(t);
   }

Ok, at this point we have the corners of the rotated rectangle in usable coordinates. We want to find the width and the height of these coordinates:

 // get extents for x,y in global SVG space:
 var x = d3.extent(corners, function(d) { return d.x; });
 var y = d3.extent(corners, function(d) { return d.y; });
 var w = x[1] - x[0];
 var h = y[1] - y[0];    

This gives us the rotated width and height, which we can now use to scale the SVG:. I'm using a small margin in my scaling function here (10% margin on each side):

// For getting scale factor:
var scale = function(elementWidth,elementHeight,SVGWidth,SVGHeight) {
  return Math.min(SVGWidth/elementWidth*0.8,SVGHeight/elementHeight*0.8);
}

Which I use as so:

var k = scale(w,h,SVGwidth,SVGheight);

At this point I can zoom to the rectangle with a translate of -x[0]*k, -y[0]*k and a scale of k. However, this will result in the shape in the top left corner of the SVG, I want to account for the margin and whatever of rectangle width/height is smaller relative to SVG width/height. For this I calculate an offset for each of x, y:

// Offset to center feature:
var ox = (SVGwidth - w*k)/2;
var oy = (SVGheight- h*k)/2;

And now I can create my transform to center a given rectangle in the SVG:

var newTransform = "translate("+[-x[0]*k+ox,-y[0]*k+oy]+")scale("+k+")";

These snippets use a parent g to hold everything in the SVG, this makes it easier to manage the transforms. Also, this g has it's transform wiped when zooming to a rectangle because we don't want the relative transform from the current zoom state/transform, but one to the original zoom state/transform, otherwise we'll see odd behavior.

Here's an example with a zoom behavior allowing you to zoom/pan manually, but with the option to click to zoom to a rectangle:

var svg = d3.select("svg");
var contents = svg.html();
svg.selectAll("*").remove();
var g = svg.append("g")
   .attr("transform", "translate(0,0)scale(1)")
   .html(contents);

var SVGwidth = 400;
var SVGheight = 180;

// For getting scale factor:
var scale = function(elementWidth,elementHeight,SVGWidth,SVGHeight) {
  return Math.min(SVGWidth/elementWidth*0.8,SVGHeight/elementHeight*0.8);
}

var zoom = d3.zoom()
  .on("zoom", function(d) {
    g.attr("transform",d3.event.transform);
  })

svg.call(zoom);

function zoomToRect(rect) {

        // Bounds in local coordinates space:
        var bounds = rect.getBBox();
        
        // Corners of rect in local coordinate space:
        var corners = [
          {x: bounds.x,y: bounds.y},
          {x: bounds.x + bounds.width, y: bounds.y},
          {x: bounds.x + bounds.width, y: bounds.y + bounds.height },
          {x: bounds.x, y: bounds.y + bounds.height }
        ];

        // Reset transform:
        g.attr("transform","");
     
        // relevant transform:
        var t = rect.getCTM();
        
        // Convert the points to global SVG coordainte space:
        for(var i = 0; i < corners.length; i++) {
          var point = svg.node().createSVGPoint();
          point.x = corners[i].x;
          point.y = corners[i].y;
          corners[i] = point.matrixTransform(t);
        }
        
        // get extents for x,y in global SVG space:
        var x = d3.extent(corners, function(d) { return d.x; });
        var y = d3.extent(corners, function(d) { return d.y; });
        var w = x[1] - x[0];
        var h = y[1] - y[0]; 
        var k = scale(w,h,SVGwidth,SVGheight);
        // Offset to center feature:
        var ox = (SVGwidth - w*k)/2;
        var oy = (SVGheight- h*k)/2;
         
        var newTransform = d3.zoomIdentity
          .translate(-x[0]*k+ox,-y[0]*k+oy)
          .scale(k);
          
        svg.call(zoom.transform,newTransform);              
        
  }
  
  
d3.select("button").on("click", function() { zoomToRect(d3.select("rect").node()) });
svg {
  border: 1px dotted black;
}
div {
  position: absolute;
  top: 0;
  left: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="180" style="background: url(&quot;/images/001.jpg&quot;) no-repeat;">   
<g id="228" transform="translate(2001,620)rotate(216) translate(0 -216)">
  <g>
    <rect x="0" y="0" height="72" width="36" stroke="black" style="fill: transparent;"></rect>
    <text x="0" y="36" text-anchor="start" style="fill: black;">228</text>
  </g>
  <g>
     <rect x="36" y="0" height="72" width="36" stroke="black" style="fill: transparent;">
     </rect><text x="36" y="36" text-anchor="start" style="fill: black;">229</text>
  </g>
</g>
<g id="228" transform="translate(1801,550)rotate(65) translate(0 0)">
   <g>
    <rect x="0" y="0" height="100" width="20" stroke="black" style="fill: transparent;"></rect>
    <text x="0" y="36" text-anchor="start" style="fill: black;">454</text>
  </g>
  <g>
     <rect x="36" y="0" height="20" width="100" stroke="black" style="fill: transparent;">
     </rect><text x="36" y="36" text-anchor="start" style="fill: black;">455</text>
  </g> 
</g>

 </svg>
 <div><button>Zoom to 228</button></div>

Here's a snippet that is just zooming around without a zoom behavior, cycling between four rectangles using your SVG plus two bonus rectangles:

var svg = d3.select("svg");
var contents = svg.html();
svg.selectAll("*").remove();
var g = svg.append("g")
   .attr("transform", "translate(0,0)scale(1)")
   .html(contents);

var SVGwidth = 400;
var SVGheight = 180;

// For getting scale factor:
var scale = function(elementWidth,elementHeight,SVGWidth,SVGHeight) {
  return Math.min(SVGWidth/elementWidth*0.8,SVGHeight/elementHeight*0.8);
}

var current = 0;

function transition() {
     g.selectAll("rect").each(function(_,i,n) {
        if(i != current) return;
       
        
        // Bounds in local coordinates space:
        var bounds = n[i].getBBox();
        
        // Corners of rect in local coordinate space:
        var corners = [
          {x: bounds.x,y: bounds.y},
          {x: bounds.x + bounds.width, y: bounds.y},
          {x: bounds.x + bounds.width, y: bounds.y + bounds.height },
          {x: bounds.x, y: bounds.y + bounds.height }
        ];

        // Hold original transform:
        var oldTransform = g.attr("transform");
        // Reset transform:
        g.attr("transform","");
     
        // relevant transform:
        var t = n[i].getCTM();
        
        // Convert the points to global SVG coordainte space:
        for(var i = 0; i < corners.length; i++) {
          var point = svg.node().createSVGPoint();
          point.x = corners[i].x;
          point.y = corners[i].y;
          corners[i] = point.matrixTransform(t);
        }
        
        // get extents for x,y in global SVG space:
        var x = d3.extent(corners, function(d) { return d.x; });
        var y = d3.extent(corners, function(d) { return d.y; });
        var w = x[1] - x[0];
        var h = y[1] - y[0]; 
        var k = scale(w,h,SVGwidth,SVGheight);
        // Offset to center feature:
        var ox = (SVGwidth - w*k)/2;
        var oy = (SVGheight- h*k)/2;
         
        var newTransform = "translate("+[-x[0]*k+ox,-y[0]*k+oy]+")scale("+k+")"
                
        g.attr("transform",oldTransform)
          .transition()
          .on("start", function() {
           d3.select(n[current]).style("fill","orange");
          })
          .duration(2000)
          .attr("transform",newTransform)
          .on("end", function() {
             d3.select(n[current]).style("fill","none");
             current++;
             current = current%g.selectAll("rect").size();
             transition();         
          })

     })

  }

transition();
svg {
  border: 1px dotted black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="180" style="background: url(&quot;/images/001.jpg&quot;) no-repeat;">   
<g id="228" transform="translate(2001,620)rotate(216) translate(0 -216)">
  <g>
    <rect x="0" y="0" height="72" width="36" stroke="black" style="fill: transparent;"></rect>
    <text x="0" y="36" text-anchor="start" style="fill: black;">228</text>
  </g>
  <g>
     <rect x="36" y="0" height="72" width="36" stroke="black" style="fill: transparent;">
     </rect><text x="36" y="36" text-anchor="start" style="fill: black;">229</text>
  </g>
</g>
<g id="228" transform="translate(1801,550)rotate(65) translate(0 0)">
   <g>
    <rect x="0" y="0" height="100" width="20" stroke="black" style="fill: transparent;"></rect>
    <text x="0" y="36" text-anchor="start" style="fill: black;">454</text>
  </g>
  <g>
     <rect x="36" y="0" height="20" width="100" stroke="black" style="fill: transparent;">
     </rect><text x="36" y="36" text-anchor="start" style="fill: black;">455</text>
  </g> 
</g>

 </svg>

Related