37

I have a complex set of shapes that are in the snippet. They are rendered with React but I'm really just looking for some pointers about how I would go about being able to zoom these shapes in and out.

My googling is failing and I can only really find examples of graphs.

How can zoom in and out complex structures like this?

    <svg height="767" width="903">
    <g class="vx-group vx-tree" transform="translate(20, 70)">
        <g class="vx-group" transform="translate(0, 70)">
            <g class="vx-group" transform="translate(0, 0)">
                <path class="vx-link-vertical" d="M451.5,0C451.5,233.5,451.5,233.5,451.5,467" percent="0.5"
                      stroke="#f7f7f3" stroke-width="1" stroke-opacity="0.2" fill="none"></path>
            </g>
            <g class="vx-group" transform="translate(0, 0)">
                <g class="vx-group" transform="translate(451.5, 0)" opacity="1">
                    <g class="vx-group node__container" transform="translate(0, 0)">
                        <svg class="" x="0" y="0" style="overflow: visible;">
                            <polygon
                                    points="25.98076211353316,-14.999999999999998 25.98076211353316,14.999999999999998 1.83697019872103e-15,30 -25.98076211353316,14.999999999999998 -25.980762113533157,-15.000000000000004 -5.510910596163089e-15,-30"
                                    class="node__hexagon"></polygon>
                        </svg>
                        <g class="vx-group node__business-unit" transform="translate(0, 0)">
                            <use xlink:href="#icon-BusinessUnit"></use>
                        </g>
                        <g class="hierarchy-label__container" transform="translate(0, -40)">
                            <path class="" d="
                                  M 0.0078125, 5.15625
                                  L 34.64882865137755,25.156249999999996 
                                  M -0.9921875, 5.15625 
                                  L -34.63320365137754,25.156249999999996
                                  H -65.8515625 
                                  a8,8 0 0 1 -8,-8  
                                  V -47.15625 
                                  a8,8 0 0 1 8,-8 H 65.8515625 a8,8 0 0 1 8,8 
                                  L 73.8515625, 17.156249999999996  
                                  a8,8 0 0 1 -8,8 
                                  L 34.64882865137755, 25.156249999999996 
                                  Z 
                                 ">
                            </path>
                            <svg x="0" y="0" style="overflow: visible;">
                                <text class="hierarchy-label__item__name" width="150" y="-25" x="0" text-anchor="middle"
                                      style="pointer-events: none;">
                                    <tspan x="0" dy="0em">Finance</tspan>
                                </text>
                            </svg>
                            <svg x="0" y="0" style="overflow: visible;">
                                <text class="hierarchy-label__item__type" width="150" y="-5" x="0" text-anchor="middle"
                                      style="pointer-events: none;">
                                    <tspan x="0" dy="0.71em">Business Unit</tspan>
                                </text>
                            </svg>
                        </g>
                    </g>
                </g>
            </g>
        </g>
    </g>
</svg>
JJJ
  • 32,902
  • 20
  • 89
  • 102
dagda1
  • 26,856
  • 59
  • 237
  • 450
  • 2
    set a viewBox on the outer svg element and alter it to zoom – Robert Longson Sep 30 '18 at 09:07
  • I think the best way to deal with `SVG`s is converting them to a font. see my [answer](https://stackoverflow.com/questions/52576376/how-to-zoom-in-on-a-complex-svg-structure#answer-52682827) – AmerllicA Oct 07 '18 at 04:05

5 Answers5

60

Scaling in svg is done with viewBox, which combines both scaling and offset. There is a nice article How to Scale SVG. From the following article:

If you think of the document as a canvas, the view box is part of the canvas you want the viewer to see.

It's like a screen of your cell phone in the camera app which shows part of the scene which is observed with specified scale and offsets.

enter image description here

Nice sample which demonstrates what is viewBox can be found here.

A little math and I implemented zoom in/zoom out with mousewheel. In addition added panning with mousemove and display scale value. An example which demonstrates how viewBox can be used:

const svgImage = document.getElementById("svgImage");
const svgContainer = document.getElementById("svgContainer");

var viewBox = {x:0,y:0,w:svgImage.clientWidth,h:svgImage.clientHeight};
svgImage.setAttribute('viewBox', `${viewBox.x} ${viewBox.y} ${viewBox.w} ${viewBox.h}`);
const svgSize = {w:svgImage.clientWidth,h:svgImage.clientHeight};
var isPanning = false;
var startPoint = {x:0,y:0};
var endPoint = {x:0,y:0};;
var scale = 1;

svgContainer.onmousewheel = function(e) {
   e.preventDefault();
   var w = viewBox.w;
   var h = viewBox.h;
   var mx = e.offsetX;//mouse x  
   var my = e.offsetY;    
   var dw = w*Math.sign(e.deltaY)*0.05;
   var dh = h*Math.sign(e.deltaY)*0.05;
   var dx = dw*mx/svgSize.w;
   var dy = dh*my/svgSize.h;
   viewBox = {x:viewBox.x+dx,y:viewBox.y+dy,w:viewBox.w-dw,h:viewBox.h-dh};
   scale = svgSize.w/viewBox.w;
   zoomValue.innerText = `${Math.round(scale*100)/100}`;
   svgImage.setAttribute('viewBox', `${viewBox.x} ${viewBox.y} ${viewBox.w} ${viewBox.h}`);
}


svgContainer.onmousedown = function(e){
   isPanning = true;
   startPoint = {x:e.x,y:e.y};   
}

svgContainer.onmousemove = function(e){
   if (isPanning){
  endPoint = {x:e.x,y:e.y};
  var dx = (startPoint.x - endPoint.x)/scale;
  var dy = (startPoint.y - endPoint.y)/scale;
  var movedViewBox = {x:viewBox.x+dx,y:viewBox.y+dy,w:viewBox.w,h:viewBox.h};
  svgImage.setAttribute('viewBox', `${movedViewBox.x} ${movedViewBox.y} ${movedViewBox.w} ${movedViewBox.h}`);
   }
}

svgContainer.onmouseup = function(e){
   if (isPanning){ 
  endPoint = {x:e.x,y:e.y};
  var dx = (startPoint.x - endPoint.x)/scale;
  var dy = (startPoint.y - endPoint.y)/scale;
  viewBox = {x:viewBox.x+dx,y:viewBox.y+dy,w:viewBox.w,h:viewBox.h};
  svgImage.setAttribute('viewBox', `${viewBox.x} ${viewBox.y} ${viewBox.w} ${viewBox.h}`);
  isPanning = false;
   }
}

svgContainer.onmouseleave = function(e){
 isPanning = false;
}
<span id="zoomValue">1</span>
<div id="svgContainer">
<svg id="svgImage" height="964" width="767">
    <g  class="vx-group vx-tree" transform="translate(20, 70)">
        <g class="vx-group" transform="translate(0, 70)">
            <g class="vx-group" transform="translate(0, 0)">
                <path class="vx-link-vertical" d="M451.5,0C451.5,233.5,451.5,233.5,451.5,467" percent="0.5" stroke="#f7f7f3" stroke-width="1" stroke-opacity="0.2" fill="none"></path>
            </g>
            <g class="vx-group" transform="translate(0, 0)">
                <g class="vx-group" transform="translate(451.5, 0)" opacity="1">
                    <g class="vx-group node__container" transform="translate(0, 0)">
                        <svg class="" x="0" y="0" style="overflow: visible;">
                            <polygon points="25.98076211353316,-14.999999999999998 25.98076211353316,14.999999999999998 1.83697019872103e-15,30 -25.98076211353316,14.999999999999998 -25.980762113533157,-15.000000000000004 -5.510910596163089e-15,-30" class="node__hexagon"></polygon>
                        </svg>
                        <g class="vx-group node__business-unit" transform="translate(0, 0)">
                            <use xlink:href="#icon-BusinessUnit"></use>
                        </g>
                        <g class="hierarchy-label__container" transform="translate(0, -40)">
                           <path class="" d="
                              M 0.0078125, 5.15625
                              L 34.64882865137755,25.156249999999996 
                              M -0.9921875, 5.15625 
                              L -34.63320365137754,25.156249999999996
                              H -65.8515625 
                              a8,8 0 0 1 -8,-8  
                              V -47.15625 
                              a8,8 0 0 1 8,-8 H 65.8515625 a8,8 0 0 1 8,8 
                              L 73.8515625, 17.156249999999996  
                              a8,8 0 0 1 -8,8 
                              L 34.64882865137755, 25.156249999999996 
                              Z 
                             "></path>
                  <svg x="0" y="0" style="overflow: visible;">
                      <text class="hierarchy-label__item__name" width="150" y="-25" x="0" text-anchor="middle" style="pointer-events: none;">
                          <tspan x="0" dy="0em">Finance</tspan>
                      </text>
                  </svg>
                  <svg x="0" y="0" style="overflow: visible;">
                      <text class="hierarchy-label__item__type" width="150" y="-5" x="0" text-anchor="middle" style="pointer-events: none;">
                          <tspan x="0" dy="0.71em">Business Unit</tspan>
                      </text>
                  </svg>
                       </g>
                   </g>
               </g>
           </g>
       </g>
   </g>
   </svg>
</div>

Math:

enter image description here

sonntam
  • 358
  • 1
  • 2
  • 14
Access Denied
  • 8,723
  • 4
  • 42
  • 72
  • 1
    nice answer is there a concept like responsive breakpoints only for zoom? I want to display different elements dependent on how far in or out the user has zoomed. Is there any scale to judge where you are in the zoom or z axis? – dagda1 Oct 04 '18 at 15:11
  • @dagda1 I added panning with mousemove and display zoom. So that you can check 'scale' variable and decide where you want your viewBox to move. – Access Denied Oct 05 '18 at 04:48
  • @dagda1 Check out latest script. Fixed error with mouse position movement during scaling. – Access Denied Oct 07 '18 at 16:28
  • do you have any links or anything about the maths applied here? I would love to understand this better – dagda1 Apr 26 '19 at 07:25
  • @dagda1 actually math was done on the paper, that's why I added last picture. – Access Denied Apr 26 '19 at 10:54
  • 2
    Thank you for this post! It got me started really quickly on zoom & pan functionality! I used it in my node-red package about state-machines [here](https://github.com/sonntam/node-red-contrib-xstate-machine/blob/master/src/smxstate-client-util.js). One small addition: The code has problems if width/height is given with a unit such as "pt" or "px". I had to use the `.clientWidth` and `.clientHeight` DOM properties instead to make it work correctly in my use-case. – sonntam Apr 05 '20 at 17:22
  • 2
    @sonntam Glad it was helpful, took me several hours to implement the code and few more hours to fix bugs. Feel free to edit my answer, I will approve. – Access Denied Apr 06 '20 at 06:44
  • 1
    This was super helpful, thank you! I had to change `onmousewheel` to `onwheel` to get the zoom to work. Just mentioning in case that helps anyone else. – Evan Jul 24 '20 at 18:54
  • Great answer. The newer apis for the event handler has changed. The e.x and e.y are now e.movementX and e.movementY. – Pierre Sep 04 '22 at 10:21
25

You can simply use transform to scale the . Set the transform-origin from where you want to "pin" the zoom origin and use scale(x) in transform like in the above example that uses a range input element with minimum value to 1 and maximum to 200 for scalling from 1% to 200%:

const slider = document.getElementById("zoomRange");
const zvgZoom = document.getElementById("svgZoom");
const zoomValue = document.getElementById("zoomValue");

slider.oninput = function() {
    //console.log('zoom', this.value / 100);
    zoomValue.innerText = `${this.value}%`;
    zvgZoom.style.transform = `scale(${this.value / 100})`;
}
#svgContainer {
    background-color: #dedede;
}

#svgZoom {
    transform-origin: 0% 0%;
}
<input type="range" min="1" max="200" value="100" class="slider" id="zoomRange">
<span id="zoomValue">100%</span>

<div id="svgContainer">
    <svg id="svgZoom" height="767" width="903">
        <g  class="vx-group vx-tree" transform="translate(20, 70)">
            <g class="vx-group" transform="translate(0, 70)">
                <g class="vx-group" transform="translate(0, 0)">
                    <path class="vx-link-vertical" d="M451.5,0C451.5,233.5,451.5,233.5,451.5,467" percent="0.5" stroke="#f7f7f3" stroke-width="1" stroke-opacity="0.2" fill="none"></path>
                </g>
                <g class="vx-group" transform="translate(0, 0)">
                    <g class="vx-group" transform="translate(451.5, 0)" opacity="1">
                        <g class="vx-group node__container" transform="translate(0, 0)">
                            <svg class="" x="0" y="0" style="overflow: visible;">
                                <polygon points="25.98076211353316,-14.999999999999998 25.98076211353316,14.999999999999998 1.83697019872103e-15,30 -25.98076211353316,14.999999999999998 -25.980762113533157,-15.000000000000004 -5.510910596163089e-15,-30" class="node__hexagon"></polygon>
                            </svg>
                            <g class="vx-group node__business-unit" transform="translate(0, 0)">
                                <use xlink:href="#icon-BusinessUnit"></use>
                            </g>
                            <g class="hierarchy-label__container" transform="translate(0, -40)">
                               <path class="" d="
                                  M 0.0078125, 5.15625
                                  L 34.64882865137755,25.156249999999996 
                                  M -0.9921875, 5.15625 
                                  L -34.63320365137754,25.156249999999996
                                  H -65.8515625 
                                  a8,8 0 0 1 -8,-8  
                                  V -47.15625 
                                  a8,8 0 0 1 8,-8 H 65.8515625 a8,8 0 0 1 8,8 
                                  L 73.8515625, 17.156249999999996  
                                  a8,8 0 0 1 -8,8 
                                  L 34.64882865137755, 25.156249999999996 
                                  Z 
                                 "></path>
                      <svg x="0" y="0" style="overflow: visible;">
                          <text class="hierarchy-label__item__name" width="150" y="-25" x="0" text-anchor="middle" style="pointer-events: none;">
                              <tspan x="0" dy="0em">Finance</tspan>
                          </text>
                      </svg>
                      <svg x="0" y="0" style="overflow: visible;">
                          <text class="hierarchy-label__item__type" width="150" y="-5" x="0" text-anchor="middle" style="pointer-events: none;">
                              <tspan x="0" dy="0.71em">Business Unit</tspan>
                          </text>
                      </svg>
                           </g>
                       </g>
                   </g>
               </g>
           </g>
       </g>
   </svg>
</div>
Christos Lytras
  • 36,310
  • 4
  • 80
  • 113
4

Zooming and panning are common and useful techniques in data visualization, which work particularly well with SVG based visualization since vector graphic does not suffer from pixelation as its bitmap counterpart would.

This answer explores D3's built-in support for both zooming and panning, by Nick Qi Zhu's book Data Visualization with D3.js Cookbook (2013).

Start by opening your local copy of the following file in your web browser:

https://github.com/NickQiZhu/d3-cookbook/blob/master/src/chapter10/zoom.html.

In this recipe will implement geometric zooming and panning using D3 zoom support. Let's see how this is done in code:

<script type="text/javascript">
var width = 960, height = 500, r = 50;

var data = [
    [width / 2 - r, height / 2 - r],
    [width / 2 - r, height / 2 + r],
    [width / 2 + r, height / 2 - r],
    [width / 2 + r, height / 2 + r]
];

var svg = d3.select("body").append("svg")
        .attr("width", width)
        .attr("height", height)
        .call(
            d3.behavior.zoom()
                .scaleExtent([1, 10])
                .on("zoom", zoom)
        )
        .append("g");

svg.selectAll("circle")
        .data(data)
        .enter().append("circle")
        .attr("r", r)
        .attr("transform", function (d) {
            return "translate(" + d + ")";
        });

function zoom() {
    svg.attr("transform", "translate(" 
        + d3.event.translate 
        + ")scale(" + d3.event.scale + ")");
}
</script>

This recipe generates the following zooming and panning effect:

Original:

Original

Zoom:

Zoom

Pan:

Pan

You will notice zooming and panning reacts perfectly well to both mouse wheel and multi-touch gesture (as opposing to the other answers). Most of the heavy lifting is done by D3 library so we needed little code.

Tiago Martins Peres
  • 14,289
  • 18
  • 86
  • 145
3

When I failed to find leightweight script for zooming (none of the tested were able to keep mouse cursor as centerpoint), I made "a bit" research and ended up with a solution my own. I found the easiest way to use viewbox.

Outer svg must have viewBox declared (or it should be created at the beginning of function).

<svg id="svgImage" width="900" height="500" viewBox="0 0 900 500">
<style>text { fill: white; }</style>
<g id="par" class="vx-group vx-tree" transform="translate(20, 70)">

    <g class="vx-group" transform="translate(0, 70)">
        <g class="vx-group" transform="translate(0, 0)">
            <path class="vx-link-vertical" d="M451.5,0C451.5,233.5,451.5,233.5,451.5,467" percent="0.5" stroke="#f7f7f3" stroke-width="1" stroke-opacity="0.2" fill="none"></path>
        </g>
        <g class="vx-group" transform="translate(0, 0)">
            <g class="vx-group" transform="translate(451.5, 0)" opacity="1">
                <g class="vx-group node__container" transform="translate(0, 0)">
                    <svg class="" x="0" y="0" style="overflow: visible;">
                        <polygon points="25.98076211353316,-14.999999999999998 25.98076211353316,14.999999999999998 1.83697019872103e-15,30 -25.98076211353316,14.999999999999998 -25.980762113533157,-15.000000000000004 -5.510910596163089e-15,-30" class="node__hexagon"></polygon>
                    </svg>
                    <g class="vx-group node__business-unit" transform="translate(0, 0)">
                        <use xlink:href="#icon-BusinessUnit"></use>
                    </g>
                    <g class="hierarchy-label__container" transform="translate(0, -40)">
                        <path class="" d="
                        M 0.0078125, 5.15625
                        L 34.64882865137755,25.156249999999996 
                        M -0.9921875, 5.15625 
                        L -34.63320365137754,25.156249999999996
                        H -65.8515625 
                        a8,8 0 0 1 -8,-8  
                        V -47.15625 
                        a8,8 0 0 1 8,-8 H 65.8515625 a8,8 0 0 1 8,8 
                        L 73.8515625, 17.156249999999996  
                        a8,8 0 0 1 -8,8 
                        L 34.64882865137755, 25.156249999999996 
                        Z 
                        "></path>
                        <svg x="0" y="0" style="overflow: visible;"><text class="hierarchy-label__item__name" width="150" y="-25" x="0" text-anchor="middle" style="pointer-events: none;"><tspan x="0" dy="0em">Finance</tspan></text></svg>
                        <svg x="0" y="0" style="overflow: visible;"><text class="hierarchy-label__item__type" width="150" y="-5" x="0" text-anchor="middle" style="pointer-events: none;"><tspan x="0" dy="0.71em">Business Unit</tspan></text></svg>
                    </g>
                </g>
            </g>
        </g>
    </g>
</g>
</svg>

<script>
const svgImage = document.getElementById("svgImage");
const svgSize = {w:svgImage.clientWidth ,h:svgImage.clientHeight};
var oldScale = 1;

svgImage.onmousewheel = function(e) {
    e.preventDefault();
    
    var svgW     = svgSize.w,
        svgH     = svgSize.h,
        mX       = e.offsetX,
        mY       = e.offsetY,
        delta    = (e.wheelDelta) ? -e.wheelDelta : e.detail,
        newScale = oldScale + (oldScale*delta/1200); //1200: intensity

    var vb      = svgImage.getAttribute('viewBox').split(" ");
    var newW    = svgW * newScale,
        newH    = svgH * newScale,
        newX    = vb[0]*1 + (vb[2]*1 - newW) * (mX/svgW),
        newY    = vb[1]*1 + (vb[3]*1 - newH) * (mY/svgH);

    viewBox = { x:Math.round(newX), y:Math.round(newY), w:newW, h:newH };
    svgImage.setAttribute('viewBox', `${viewBox.x} ${viewBox.y} ${viewBox.w} ${viewBox.h}`);
    oldScale = newScale;
}
</script>

PS. Very good series of articles about SVG coordinate system manipulation (and not only) is written by Sara Soueidan. Good blog to dig around.

Sven Liivak
  • 1,323
  • 9
  • 10
0

const svgImage=document.getElementById("svgImage");
function a(val){
   svgImage.setAttribute('viewBox',val);
}
.bt{height:40px}
<button onclick="a('0 0 6000 6000')" class="bt">Resize</button>
<button onclick="a('0 0 368 368')" class="bt">Reset</button>
<svg id="svgImage" id="svgImage" viewBox="0 0 368 368" width="100%" height="100%">
<circle fill="#444" cx="184" cy="184" r="184"/>
<polygon fill="#ccc" points="202.1,199.1 202.8,283.25 157.6,283.25 157.6,199.81 73.45,199.1 73.45,153.85 157.60,154.56 157.6,69.7 202.859424,69.7 202.86,154.56 286.29805,154.56 287,199.1"/>
</svg>
Vitalicus
  • 1,188
  • 13
  • 15