Way to calculate zoom and pan using canvas and d3.js hierarchical circle structure

I currently have a d3 hierarchical circle pack graph in canvas (with help from this awesome tutorial!)

I wanted to make the graph also pan with the mouse and scale with the wheel in addition to zooming in to a node. I used this.

Now, when I first pan and then zoom to a node using the initial scale (When the page first loads), it works as expected. It zooms to the node from wherever the graph’s last panned position was.

But when I try to pan and zoom from the zoomed state, the view starts at a completely different place before it ends at the expected node. If from the zoomed state I don’t pan, but jump/zoom from one node to another, the transition works as expected. I’m not sure why this happens. Can anyone help me figure out what I’m doing wrong? Thanks in advance!

(Sorry for any faux-pas — Its my first stack overflow question…)


  @ViewChild('myCanvas') canvas: ElementRef;
  @ViewChild('myCanvas') hiddenCanvas: ElementRef;
  public context: CanvasRenderingContext2D;
  public hiddenContext: CanvasRenderingContext2D;

  public focus;
  public xleftView = 0;
  public ytopView = 0;

  public widthViewOriginal = window.innerWidth;           
  public heightViewOriginal = window.innerHeight;
  public widthView = this.widthViewOriginal;           
  public heightView = this.heightViewOriginal;
  public widthCanvas = window.innerWidth;
  public heightCanvas = window.innerHeight;

  public mouseDown = false;
  public movedCoordinatesX;
  public movedCoordinatesY;
  public lastX = 0;
  public lastY = 0;

  public zoomInfo = {
    centerX: window.innerWidth / 2,
    centerY: window.innerHeight / 2,
    scale: 1
  };

    public handleMouseDown(event) {
      // get where the mouse first clicked to pan the graph
      this.movedCoordinatesX = event.clientX
      this.movedCoordinatesY = event.clientY
      if (event.which === 1) {
        this.mouseDown = true;
      }
    }

    public handleMouseUp(event) {
      this.mouseDown = false;
      // Get the distance moved in the x & y direction from the initial click to the end mouse position
      this.movedCoordinatesX = this.movedCoordinatesX - event.clientX
      this.movedCoordinatesY =  this.movedCoordinatesY - event.clientY

      // add distance in the X & Y to the old view's X & Y (to make the transition happen from the current position
      this.vOld[0] = (this.vOld[0] + this.movedCoordinatesX) 
      this.vOld[1] = (this.vOld[1] + this.movedCoordinatesY) 
    }

    public handleMouseMove(event) {

      var X = event.clientX - this.context.canvas.offsetLeft -  this.context.canvas.clientLeft + this.context.canvas.scrollLeft;
      var Y = event.clientY -  this.context.canvas.offsetTop -  this.context.canvas.clientTop +  this.context.canvas.scrollTop;
      if (this.mouseDown) {
          var dx = (X - this.lastX) / this.widthCanvas * this.widthView;
          var dy = (Y - this.lastY)/ this.heightCanvas * this.heightView;
          this.xleftView -= dx;
          this.ytopView -= dy;
      }
      this.lastX = X;
      this.lastY = Y;
  }

  // Listen for clicks on the main canvas
  public zoomInToNode(e) {
    // We actually only need to draw the hidden canvas when there is an interaction. 
    // This sketch can draw it on each loop, but that is only for demonstration.
    this.drawCanvas(this.hiddenContext, true);

    //Figure out where the mouse click occurred.
    var mouseX = e.layerX;
    var mouseY = e.layerY;
    this.lastX = mouseX
    this.lastY = mouseY

    // Get the corresponding pixel color on the hidden canvas and look up the node in our map.
    // This will return that pixel's color
    var col = this.hiddenContext.getImageData(mouseX, mouseY, 1, 1).data;
    //Our map uses these rgb strings as keys to nodes.
    var colString = "rgb(" + col[0] + "," + col[1] + ","+ col[2] + ")";
    var node = this.colToCircle[colString];

    if(node) {
      if (this.focus !== node) {

        this.xleftView = 0
        this.ytopView = 0
        this.hiddenContext.setTransform(1, 0, 0, 1, 0, 0);
        this.zoomToCanvas(node); 
      }
      else {

        this.xleftView = 0
        this.ytopView = 0
        this.hiddenContext.setTransform(1, 0, 0, 1, 0, 0);

        this. widthView = this.widthViewOriginal; 
        this. heightView = this.heightViewOriginal;
        this. widthCanvas = window.innerWidth;
        this. heightCanvas = window.innerHeight;
        this.zoomToCanvas(this.rooting);
      }
    }//if
  };

  public zoomToCanvas(focusNode) {
    // the node that we want to zoom to
    this.focus = focusNode;

    var v = [this.focus.x, this.focus.y, this.focus.r * 2.05]; //The center and width of the new "viewport"

    this.interpolator = d3.interpolateZoom(this.vOld, v); //Create interpolation between current and new "viewport"

    this.duration = this.interpolator.duration; //Interpolation gives back a suggested duration         
    this.timeElapsed = 0; //Set the time elapsed for the interpolateZoom function to 0  
    this.vOld = v; //Save the "viewport" of the next state as the next "old" state

  }//function zoomToCanvas

  // Perform the interpolation and continuously change the zoomInfo while the "transition" occurs
  public interpolateZoom(dt) {
    if (this.interpolator) {
      this.timeElapsed += dt;
      var t = this.ease(this.timeElapsed / this.duration);

      if(isFinite(t)) { 
      this.zoomInfo.centerX = this.interpolator(t)[0];
      this.zoomInfo.centerY = this.interpolator(t)[1];
      this.zoomInfo.scale = this.diameter / this.interpolator(t)[2];

      if (this.timeElapsed >= this.duration) this.interpolator = null;
      }
    }//if
  }//function zoomToCanvas

  public drawCanvas(chosenContext, hidden) {
    chosenContext.save(); 
    chosenContext.setTransform(1,0,0,1,0,0);
    chosenContext.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
    chosenContext.scale(this.widthCanvas/this.widthView, this.heightCanvas/this.heightView);
    chosenContext.translate(-this.xleftView,-this.ytopView);
    chosenContext.restore(); 

  }

Source: AngularJS