Pan and Zoom in jsPlumb Community Edition with Dagre and jQueryUI Draggable

See the Pen Pan and Zoom in jsPlumb Community Edition with Dagre and jQueryUI Draggable by Yuri Gor (@yurigor) on CodePen.0

Chart with draggable HTML elements as nodes, connected by jsPlumb library.
“Pan&Zoom” feature missing in Comunity Edition implemented by using “jQuery Panzoom” plugin.
Nodes dragging implemented by jQueryUI Draggable, to compensate scale distortion.
Dagre layout library used for demonstration.

I use static predefined html in this example:

<!-- .container - just part of your page,
where you want to render diagram -->
<div class="container">
  <!-- .panzoom - wrapper div, panzoom plugin will transform it.
        Use it for worksheet element styling -->
  <div class="panzoom">
    <!-- .diagram - wrapper div to be used by jsPlumb.
         It will have zero height, so no visual CSS works here. -->
    <div class="diagram">
      <!-- .item - diagram nodes, must have unique id's
           to be able connect them by jsPlumb. -->
      <div id="i0"  class="item">Root</div>
      <div id="i1" class="item">Child 1</div>
      <div  id="i11" class="item">Child 1.1</div>
      <div  id="i12" class="item">Child 1.2</div>
      <div id="i2" class="item">Child 2</div>
      <div id="i21" class="item">Child 2.1</div>
      <div id="i3" class="item">Child 3</div>
    </div>
  </div>
</div>

Links between nodes declared in js array:

var links = [
  { from: "i0", to: "i1" },
  { from: "i1", to: "i11" },
  { from: "i1", to: "i12" },
  { from: "i0", to: "i2" },
  { from: "i2", to: "i21" },
  { from: "i0", to: "i3" },
];

Initializing of panzoom:

$panzoom = $container.find('.panzoom').panzoom({
      minScale: minScale,//0.4
      maxScale: maxScale,//2
      increment: incScale,//0.1
      cursor: "",/*empty string prevents panzoom
          from changing cursor styles defined in your css.*/
    }).on("panzoomstart",function(e,pz,ev){
      $panzoom.css("cursor","move");//set "move" cursor on start only
    })
    .on("panzoomend",function(e,pz){
      $panzoom.css("cursor","");//restore cursor
    });

Mouse wheel support and pan while drag begins outside diagram:

$panzoom.parent()
    .on('mousewheel.focal', function( e ) {
      //if Control pressed then zoom
      if(e.ctrlKey||e.originalEvent.ctrlKey)
      {
        e.preventDefault();
        var delta = e.delta || e.originalEvent.wheelDelta;
        var zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;
        $panzoom.panzoom('zoom', zoomOut, {
           animate: true,
           exponential: false,
        });
      }else{//else pan (touchpad and Shift key works)
        e.preventDefault();
        var deltaY = e.deltaY || e.originalEvent.wheelDeltaY || (-e.originalEvent.deltaY);
        var deltaX = e.deltaX || e.originalEvent.wheelDeltaX || (-e.originalEvent.deltaX);
        $panzoom.panzoom("pan",deltaX/2,deltaY/2,{
          animate: true,
          relative: true,
        });
      }
    })
    //on start store initial offsets and mouse coord
    .on("mousedown touchstart",function(ev){
      var matrix = $container.find(".panzoom").panzoom("getMatrix");
      var offsetX = matrix[4];
      var offsetY = matrix[5];
      var dragstart = {x:ev.pageX,y:ev.pageY,dx:offsetX,dy:offsetY};
      $(ev.target).css("cursor","move");
      $(this).data('dragstart', dragstart);
    })
    //calculate mouse offset from starting pos and apply it to panzoom matrix
    .on("mousemove touchmove", function(ev){
      var dragstart = $(this).data('dragstart');
      if(dragstart)
      {
        var deltaX = dragstart.x-ev.pageX;
        var deltaY = dragstart.y-ev.pageY;
        var matrix = $container.find(".panzoom").panzoom("getMatrix");
        matrix[4] = parseInt(dragstart.dx)-deltaX;
        matrix[5] = parseInt(dragstart.dy)-deltaY;
        $container.find(".panzoom").panzoom("setMatrix",matrix);
      }
    })
    .on("mouseup touchend touchcancel", function(ev){
      $(this).data('dragstart',null);
      $(ev.target).css("cursor","");
    });
  });

Make nodes draggable by jQueryUI/draggable:

var currentScale = 1;
  $container.find(".diagram .item").draggable({
    start: function(e){
      var pz = $container.find(".panzoom");
      //save current scale factor to consider it later
      currentScale = pz.panzoom("getMatrix")[0];
      $(this).css("cursor","move");
      //disable panzoom, to avoid panning while dragging node
      pz.panzoom("disable");
    },
    drag:function(e,ui){
      /*compensate current scale while dragging,
           else pointer and node will have different speeds*/
      ui.position.left = ui.position.left/currentScale;
      ui.position.top = ui.position.top/currentScale;
      //it's possible to have not connected nodes, so let's check it.
      if($(this).hasClass("jsplumb-connected"))
      {
        plumb.repaint($(this).attr('id'),ui.position);
      }
    },
    stop: function(e,ui){
      var nodeId = $(this).attr('id');
      if($(this).hasClass("jsplumb-connected"))
      {
        plumb.repaint(nodeId,ui.position);
      }
      $(this).css("cursor","");
      $container.find(".panzoom").panzoom("enable");
    }
  });

8 Replies to “Pan and Zoom in jsPlumb Community Edition with Dagre and jQueryUI Draggable”

  1. hi,this is really good job you have done。
    actually i have used jsplumb in React, SO, how can i use this in my react code?

    thanks for what you have done

  2. Hey Yuri,

    Was wondering, is the issue with child nodes not being movable still a problem natively in JqueryUI?

      1. I have pretty much replicated your code in my own project for this, and I used your forked repo, but I still cant seem to click on child elements of my panzoom container. All clicks in the panzoom container are read as clicks on the panzoom container.

  3. Thanks for the code, nice work 🙂

    I had one issue when using Shift+Mouse wheel to pan left/right. After adding the following code it works 🙂

    .on(‘mousewheel.focal’, function( e ) {
    if(e.ctrlKey||e.originalEvent.ctrlKey)
    {
    e.preventDefault();
    var delta = e.delta || e.originalEvent.wheelDelta;
    var zoomOut = delta ? delta 0;
    $panzoom.panzoom(‘zoom’, zoomOut, {
    animate: true,
    exponential: false,
    });
    }else if(e.shiftKey||e.originalEvent.shiftKey){
    e.preventDefault();
    var deltaY = e.deltaY || e.originalEvent.wheelDeltaY || (-e.originalEvent.deltaY);
    $panzoom.panzoom(“pan”,deltaY/2,0,{
    animate: true,
    relative: true,
    });
    }else{
    e.preventDefault();
    var deltaY = e.deltaY || e.originalEvent.wheelDeltaY || (-e.originalEvent.deltaY);
    var deltaX = e.deltaX || e.originalEvent.wheelDeltaX || (-e.originalEvent.deltaX);
    $panzoom.panzoom(“pan”,deltaX/2,deltaY/2,{
    animate: true,
    relative: true,
    });
    }
    })

Leave a Reply to Yuri Gor Cancel reply

Your email address will not be published. Required fields are marked *