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?
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
An example of the full code with the fix implemented https://jsfiddle.net/x2e1wrLg/