D3: keeping position of SVG's relative while panning and zooming

Dan Hoff

I'm trying to do a proof of concept of a SVG floorplan that pans and zooms and also have the ability to place markers on top. When zooming/panning happens the marker doesn't stay in position. I understand why this happens but not sure about the best way to keep the marker in position when panning/zooming.

Heres the code:

    var svg = d3.select(".floorplan")
  .attr("width", "100%")
  .attr("height", "100%")
  .call(d3.zoom().on("zoom", zoomed))
  .select("g")

  var marker = d3.selectAll(".marker")
    .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended)
    )


  function zoomed() {
    svg.attr("transform", d3.event.transform);
  }

  function dragstarted(d) {
    console.log('dragstarted');
  }

  function dragged(d) {
    var x = d3.event.x;
    var y = d3.event.y;

    d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
  }

  function dragended(d) {
    console.log('drag ended: marker:'+ d3.select(this).attr('data-id') + ' position: ' + d3.event.x +', ' + d3.event.y);
  }

Theres also a codepen to visually see here: https://codepen.io/danielhoff/pen/WzQbRr

I have an additional constraints that the marker element shouldn't be contained inside the floorplan svg.

Xavier Guihot

Here is a modified version of your codepen which fixes the movements of the marker during drag events while keeping the marker outside the floorplan svg container:

https://codepen.io/xavierguihot/pen/OvyRPY?editors=0010


To bring back into context, an easy solution would have been to include the marker element inside the floorplan container (in order for the marker to get the same zoom events as the floorplan), but here we want the marker to be in its own svg container.

And it is not trivial!


Appart from including ids in html tags (in order to select these elements from the html), only the javascript part has ben modified.

Let's dig a little bit on the steps necessary to get to this point:

First: Let's modify the zoomed function to apply to the marker as well:

Initially this was the zoom function:

function zoomed() {
  svg.attr("transform", d3.event.transform);
}

And the modified version:

function zoomed() {

  // Before zooming the floor, let's find the previous scale of the floor:
  var curFloor = document.getElementById('floorplan');
  var curFloorScale = 1;
  if (curFloor.getAttribute("transform")) {
    var curFloorTransf = getTransformation(curFloor.getAttribute("transform"));
    curFloorScale = curFloorTransf.scaleX;
  }

  // Let's apply the zoom
  svg.attr("transform", d3.event.transform);

  // And let's now find the new scale of the floor:
  var newFloorTransf = getTransformation(curFloor.getAttribute("transform"));
  var newFloorScale = newFloorTransf.scaleX;

  // This way we get the diff of scale applied to the floor, which we'll apply to the marker:
  var dscale = newFloorScale - curFloorScale;

  // Then let's find the current x, y coordinates of the marker:
  var marker = document.getElementById('Layer_1');
  var currentTransf = getTransformation(marker.getAttribute("transform"));
  var currentx = currentTransf.translateX;
  var currenty = currentTransf.translateY;

  // And the position of the mouse:
  var center = d3.mouse(marker);

  // In order to find out the distance between the mouse and the marker:
  // (43 is based on the size of the marker)
  var dx = currentx - center[0] + 43;
  var dy = currenty - center[1];

  // Which allows us to find out the exact place of the new x, y coordinates of the marker after the zoom:
  // 38.5 and 39.8 comes from the ratio between the size of the floor container and the marker container.
  // "/2" comes (I think) from the fact that the floor container is initially translated at the center of the screen:
  var newx = currentx + dx * dscale / (38.5/2);
  var newy = currenty + dy * dscale / (39.8/2);

  // And we can finally apply the translation/scale of the marker!:
  d3.selectAll(".marker").attr("transform", "translate(" + newx + "," + newy + ") scale(" + d3.event.transform.k + ")");
}

This heavily uses the getTransformation function which allows to retrieve the current transform details of an element.

Then: But now, after having zoomed, when we drag the marker, it takes back its original size:

This means we have to tweak the marker's dragg function to keep its current scale when applying the drag transform:

Here was the initial drag function:

function dragged(d) {
  var x = d3.event.x;
  var y = d3.event.y;
  d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
}

And its modified version:

function draggedMarker(d) {

  var x = d3.event.x;
  var y = d3.event.y;

  // As we want to keep the same current scale of the marker during the transform, let's find out the current scale of the marker:
  var marker = document.getElementById('Layer_1');
  var curScale = 1;
  if (marker.getAttribute("transform")) {
    curScale = getTransformation(marker.getAttribute("transform")).scaleX;
  }

  // We can thus apply the translate And keep the current scale:
  d3.select(this).attr("transform", "translate(" + x + "," + y + "), scale(" + curScale + ")");
}

Finally: When dragging the floor we also have to drag the marker accordingly:

We thus have to override the default dragging of the floor in order to include the same dragg event to the marker.

Here is the drag function applied to the floor:

function draggedFloor(d) {

  // Overriding the floor drag to do the exact same thing as the default drag behaviour^^:

  var dx = d3.event.dx;
  var dy = d3.event.dy;

  var curFloor = document.getElementById('svg-floor');
  var curScale = 1;
  var curx = 0;
  var cury = 0;
  if (curFloor.getAttribute("transform")) {
    curScale = getTransformation(curFloor.getAttribute("transform")).scaleX;
    curx = getTransformation(curFloor.getAttribute("transform")).translateX;
    cury = getTransformation(curFloor.getAttribute("transform")).translateY;
  }

  d3.select(this).attr("transform", "translate(" + (curx + dx) + "," + (cury + dy) + ")");

  // We had to override the floor drag in order to include in the same method the drag of the marker:

  var marker = document.getElementById('Layer_1');
  var currentTransf = getTransformation(marker.getAttribute("transform"));

  var currentx = currentTransf.translateX;
  var currenty = currentTransf.translateY;
  var currentScale = currentTransf.scaleX;

  d3.selectAll(".marker").attr("transform", "translate(" + (currentx + dx) + "," + (currenty + dy) + ") scale(" + currentScale + ")");
}

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

D3: Zooming/Panning Line Graph in SVG is not working in Canvas

Issues with Datamaps D3 Pin/marker constant size and relative position while zooming

QML, Keeping the position while zooming into a ListView

D3 - Zooming with D3's SVG Symbols

d3 how to tie text to top right corner of view port while zooming and panning

Apply panning and zooming on inline SVG

Background image not zooming and panning with SVG

Keeping relative position while teleporting my player?

Implement panning while keeping nodes draggable in d3 force layout

Having trouble panning and zooming on my d3 map

Constraining zooming and panning in a D3 map viewport

D3 Canvas graph zooming and panning slow redraw in Webview

How do I get the d3 svg points to stay in their correct position on the leaflet map when zooming?

Scaling images to window size while keeping relative position to each other

Zooming and Panning in JS/JQuery like we can do it using SVG

How to move SVG's position in D3?

Edit D3 SVG's links position in a "radial" Chart

d3 v2 Prevent Zooming and Panning Chart Outside Viewport

How can I detect panning and zooming by the user with d3 zoomBehavior?

d3js zooming in one direction, panning in both directions

D3.js zooming and panning - lock on y axis

Zooming and panning an image in a QScrollArea

Zooming and panning TextureView

How to get these divs side-by-side while still keeping position:relative and not using float?

Keeping the relative position of a picturebox on another picturebox while resizing with SizeMode.Zoom, Winforms

Avoid unzooming/zooming transition while updating zoom on d3

Zooming 2 imageviews side by side while keeping proportions

Flot zooming and panning with logarithmic axis

CALayer with NSScrollView, zooming panning and clicking