Finding the correct positions for nodes when dragging an SVG element after zooming/panning in javascript

34 Views Asked by At

I am making a script that plots directed graphs using SVG from some input that is of the form

let data = {
  'A': {
    'children': ['B', 'C'],
    'parents': [],
    'coords': {
      'x': 10,
      'y': 10
    }
  },
  'B': {
    'children': ['C'],
    'parents': ['A'],
    'coords': {
      'x': 30,
      'y': 10
    }
  },
  'C': {
    'children': [],
    'parents': ['A', 'B'],
    'coords': {
      'x': 20,
      'y': 20
    }
  }
}

It creates paths between parent and child nodes by using a cubic bezier curve. The idea is to be able to construct a visualization for the graph based on the 'coords' properties of each node, then allowing the user to move the nodes around in real time by dragging and dropping them.

I got this implemented just fine until I added in the ability to pan and zoom. Now, if the image is panned and or zoomed, when I go to update the positions of elements to the cursor position they get put in the wrong location. Here are my dragging functions that I currently have to update positions

function startDrag(evt) {
    if (evt.target.classList.contains('draggable')) {
      selectedElement = evt.target;

      // we need to store the IDs of paths connecting to the nodes so that we can update their positions accordingly
      // Their IDs are stored as `${parent_key}_to_${child_key}`, e.g., #A_to_I
      path_ids = [];
      let node_key = selectedElement.getAttributeNS(null, 'id');
      for (let child_key of data[node_key]['children']) {
        path_ids.push(`${node_key}_to_${child_key}`);
      }
      for (let parent_key of data[node_key]['parents']) {
        path_ids.push(`${parent_key}_to_${node_key}`);
      }
    }
  }
  
  function drag(evt) {
    if (selectedElement) {
      evt.preventDefault();
      
      // we need zoom/pan information to reposition dragged nodes correctly
      ///////////////////////////////////////////////////
      // Potentially use some of this data to calculate correct positions ???
      let matrix = document.getElementById('scene').getAttributeNS(null, 'transform');
      let m = matrix.slice(7, matrix.length-1).split(' ');
      let zoomFactor = m[0];
      let panX = m[4];
      let panY = m[5];

      let svgBBox = svg.getBBox();
      ///////////////////////////////////////////////////

      // move the node itself
      selectedElement.setAttributeNS(null, 'cx', evt.clientX);
      selectedElement.setAttributeNS(null, 'cy', evt.clientY);
      
      // now for each path connected to the node, we need to update either the first vertex of the cubic bezier curve, or the final vertex
      // if id is ${clicked_node}_to_${other} then we change the first point, if it is ${other}_to_${clicked_node} then the last node
      let clicked_node = selectedElement.getAttributeNS(null, 'id');
      for (let path_id of path_ids) {
        let path = document.getElementById(path_id);
        let bez_d = path.getAttributeNS(null, 'd');
        let bez_split = bez_d.split(' ');
        if (path_id[0] === clicked_node) {
          let new_d = `M ${evt.clientX} ${evt.clientY} C ${evt.clientX},${evt.clientY}`;
          new_d += ` ${bez_split[5]} ${bez_split[6]}`;
          path.setAttributeNS(null, 'd', new_d);
        } else if (path_id[path_id.length - 1] === clicked_node) {
          let new_d = `M ${bez_split[1]} ${bez_split[2]} C ${bez_split[4]} ${bez_split[5]}`;
          new_d += ` ${evt.clientX},${evt.clientY}`;
          path.setAttributeNS(null, 'd', new_d);
        }
      }
    }
  }
  
  function endDrag(evt) {
    selectedElement = null;
    path_ids = [];
  }

As you can see in the drag() function, I am able to grab bbox data from the svg itself after panning/zooming, and I am able to get the transform matrix for the <g> element that houses all of my draggable nodes. I assume that the correct positions could be calculated with this information, but I am at a loss as to how.

See https://jsfiddle.net/quamjxg7/ for the full code.

Plainly put: How do I account for panning and zooming when updating the positions of draggable SVG elements?

1

There are 1 best solutions below

0
eigenVector5 On

Wow, the solution was arguably trivial.

I realized that I could handle panning by offsetting the cursor position by the translation parameters from the transform matrix of the <g> element. Then I realized that I could just divide this by the zoomingFactor to account for zooming. See the updated drag function below

function drag(evt) {
    if (selectedElement) {
      evt.preventDefault();
      
      // we need zoom/pan information to reposition dragged nodes correctly
      let matrix = document.getElementById('scene').getAttributeNS(null, 'transform');
      let m = matrix.slice(7, matrix.length-1).split(' ');
      let zoomFactor = m[0];
      let panX = m[4];
      let panY = m[5];

      let newX = (evt.clientX - panX)/zoomFactor;
      let newY = (evt.clientY - panY)/zoomFactor;

      // move the node itself
      selectedElement.setAttributeNS(null, 'cx', newX);
      selectedElement.setAttributeNS(null, 'cy', newY);
      
      // now for each path connected to the node, we need to update either the first vertex of the cubic bezier curve, or the final vertex
      // if id is ${clicked_node}_to_${other} then we change the first point, if it is ${other}_to_${clicked_node} then the last node
      let clicked_node = selectedElement.getAttributeNS(null, 'id');
      for (let path_id of path_ids) {
        let path = document.getElementById(path_id);
        let bez_d = path.getAttributeNS(null, 'd');
        let bez_split = bez_d.split(' ');
        if (path_id[0] === clicked_node) {
          let new_d = `M ${newX} ${newY} C ${newX},${newY}`;
          new_d += ` ${bez_split[5]} ${bez_split[6]}`;
          path.setAttributeNS(null, 'd', new_d);
        } else if (path_id[path_id.length - 1] === clicked_node) {
          let new_d = `M ${bez_split[1]} ${bez_split[2]} C ${bez_split[4]} ${bez_split[5]}`;
          new_d += ` ${newX},${newY}`;
          path.setAttributeNS(null, 'd', new_d);
        }
      }
    }
  }

An example of the full code with the fix implemented https://jsfiddle.net/x2e1wrLg/