1

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
  • 54,987
  • 21
  • 291
  • 190
Dan Hoff
  • 35
  • 1
  • 7

1 Answers1

1

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 + ")");
}
Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
  • 1
    Hey! Thanks for all the effort! It's appreciated! I think you are right when you say it's not trivial and I think there will be performance issues when there are multiple markers! Looking at this I think we are going to try and find a solution to append the markers in the floorplan SVG or maybe wrap both SVG's in a viewport and zoom on that level. Thanks again for all your help! – Dan Hoff Mar 13 '18 at 12:18