2

I'm trying to understand d3.drag() but I am not able to grasp the content in the drag.container() section, so I'm asking here for help.

This is the way I understand it: If I add the container function, then I can limit the area on which I can drag. If interpreted correctly, then how can I use this?

For example, in the following code snippet, I have created a circle that I can drag around. But I only want to limit the drag area inside the white circle. Can I use drag.container() to this?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://d3js.org/d3.v6.min.js"></script>

    <style>

    </style>

</head>
<body>



    <script>

        const svg = d3.select("body").append("svg").attr("width", "600").attr("height", "500").style("background", "lightblue");

        // Can I use this circle as the container?
        const circleAsContainer = svg.append("circle").attr("cx", "180").attr("cy", "200").attr("r", "120").attr("fill", "white")

        const circle = svg.append("circle").attr("cx", "120").attr("cy", "150").attr("r", "30").attr("fill", "orange").call(d3.drag() 
            .on("start", (event, d) => circle.attr("stroke", "black").attr("stroke-width", "4"))
            .on("drag", (event, d) => circle.attr("cx", d => event.x).attr("cy", d => event.y))
            .on("end", (event, d) => circle.attr("stroke", "none"))
        );


    </script>
</body>
</html>
Cmagelssen
  • 620
  • 5
  • 21

2 Answers2

3

The container element only determines the coordinate system of the event.x and event.y coordinates. That would be useful if, for instance, your draggable circle were being positioned relative to another element using the "dx" and "dy" attributes supported by some SVG elements. However, container() will not serve to automatically constrain event.x and event.y to fit within a given element's visual bounds.

To elaborate upon Ouroborus's comment: You have the following lines in your drag event handler:

circle.attr("cx", d => event.x).attr("cy", d => event.y))

In the functions "d => event.x" and "d => event.y" you must constrain event.x and event.y according to a mathematical formula corresponding to what you have referred to as your "container's" shape.

For example, to constrain the draggable element to remain within a rectangular region, the formula is simple:

 circle.attr('cx', d => {
    if (event.x > 60 + 240) { // El dragged beyond the right-hand border
      return 60 + 240
    } else if (event.x < 60) { // El dragged beyond the left-hand border
      return 60
    } else {  // El is within the box's horizontal bounds
      return event.x
    }
  })

For the y-axis, the bounding formula follows analogously. The end result looks like this (run the snippet and try it out):

    <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://d3js.org/d3.v6.min.js"></script>

    <style>

    </style>

</head>
<body>


<script>

  const svg = d3.select('body').append('svg').attr('width', '600').attr('height', '500').style('background', 'lightblue')

  const containingRect = svg.append('rect')
    .attr('x', '60')
    .attr('y', '80')
    .attr('width', '240')
    .attr('height', '240')
    .attr('fill', 'grey')
  // Can I use this circle as the container?
  const circleAsContainer = svg.append('circle').attr('cx', '180').attr('cy', '200').attr('r', '120').attr('fill', 'white')

  const circle = svg.append('circle').attr('cx', '120').attr('cy', '150').attr('r', '30').attr('fill', 'orange')
    .call(d3.drag()
      .on('start', (event, d) => circle.attr('stroke', 'black').attr('stroke-width', '4'))
      .on('drag', (event, d) =>
        circle.attr('cx', d => {
          if (event.x > 60 + 240) { // El dragged beyond the right-hand border
            return 60 + 240
          } else if (event.x < 60) { // El dragged beyond the left-hand border
            return 60
          } else {  // El is within the box's horizontal bounds
            return event.x
          }
        }).attr('cy', d => {
          if (event.y > 80 + 240) { // El dragged beyond the top border
            return 80 + 240
          } else if (event.y < 80) { // El dragged beyond the bottom border
            return 80
          } else {  // El is within the box's vertical bounds
            return event.y
          }
        })
      ).on('end', (event, d) => circle.attr('stroke', 'none'))
    )


</script>
</body>
</html>

To constrain the element to be dragged within a circular region, you must solve a different math problem. If I understand your question correctly, you would like the element to end up at the coordinates (targetX, targetY) in the event that the draggable element is dragged to (x, y) outside of the containing circular region: Visual diagram of the circular constraint problem

In this diagram, (x, y) represent event.x and event.y. (circleCenterX, circleCenterY) are the coordinates cx, cy of the bounding circle, and radius represents the radius of the bounding circle. Given these five values, I think that you would like to calculate targetX and targetY.

Luckily for you, I have solved this problem myself for a project of my own! Here is a function to calculate targetX and targetY:

/**
 * @author Ann Yanich (ann.yanich@posteo.de)
 * Based loosely on TWiStErRob's answer https://stackoverflow.com/a/31254199/7359454
 * @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
 * Finds the intersection point between:
 *     * The circle of radius at (circleCenterX, circleCenterY)
 *     and
 *     * The half line going from (x, y) to the center of the circle
 *
 * I know this works from outside the circle, but I have not taken the
 * time to reason out what would happen if (x, y) lie inside the circle.
 *
 * @param x:Number x coordinate of point to build the half-line from
 * @param y:Number y coordinate of point to build the half-line from
 * @param radius:Number the radius of the circle
 * @param circleCenterX:Number The X coordinate of the center of the circle
 * @param circleCenterY:Number The Y coordinate of the center of the circle
 * @param validate:boolean (optional) whether to treat point inside the circle as error
 * @return an object with x and y members for the intersection
 * @throws if validate == true and (x,y) is inside the circle
 */
function pointOnCircle (x, y, radius, circleCenterX, circleCenterY, validate) {
  const dx = circleCenterX - x
  const dy = circleCenterY - y
  const distance = Math.sqrt(dx * dx + dy * dy)
  if (validate && distance < radius) {
    throw new Error('Point ' + [x, y] + 'cannot be inside the circle centered at ' +
      [circleCenterX, circleCenterY] + ' with radius' + radius + '.')
  }
  const normX = dx / distance
  const normY = dy / distance
  const targetX = circleCenterX - radius * normX
  const targetY = circleCenterY - radius * normY
  return {
    x: targetX,
    y: targetY
  }
}

You would use this function as follows:

circle.attr("cx", d => {
    if (isInCircle(event.x, event.y)) {
         return event.x;
    } else {
         return pointOnCircle(event.x, event.y, radius, circleCenterX, circleCenterY, true)["x"];
    }
}).attr("cy", d => {...})

I will leave the implementation of "isInCircle()" up to you. I hope that this is enough information for you to be able to piece together a working solution :)

All the best, Ann

Ann
  • 101
  • 8
3

Here is my code. Try move blue circle outside red one~

    .active {
      stroke: #000;
      stroke-width: 2px;
  } 
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <svg width="1000" height="500" style="border: 1px solid black">
    <circle cx="250" cy="250" r="200" style="fill:none;stroke:#FF7F7F;stroke-width:16px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;z-index: 1;"/>
    <circle cx="750" cy="250" r="200" style="fill:none;stroke:#FF7F7F;stroke-width:16px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;z-index: 1;"/>
  </svg>  
  <script>
  
  var svg = d3.select("svg"),
      width = +svg.attr("width"),
      height = +svg.attr("height"),
      radius = 32; 
 
  var circles = [{},{},
               // dcx: default_center_X_coordinate
                 {dcx: 250, dcy: 250, color: '#1f77b4', id: 0}, 
                 {dcx: 750, dcy: 250, color: '#aec7e8', id: 1}]; 
  
  svg.selectAll("circle")
    .data(circles)
    .enter().append("circle")
      .attr("cx", function(d) { return d.dcx; })
      .attr("cy", function(d) { return d.dcy; })
      .attr("dcx", function(d) { return d.dcx; })
      .attr("dcy", function(d) { return d.dcy; })
      .attr("r", radius)
      .attr("id", function(d) { return d.id; })
      .style("fill", function(d, i) { return d.color; })
      .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));
   
  function dragstarted(d) {
    d3.select(this)
      .raise()
      .classed("active", true);
  }
  
  function keepInCircle(dcx, dcy, x, y, r) {
    const dx = x - dcx, 
          dy = y - dcy, 
        dist = Math.sqrt(dx * dx + dy * dy)
    return (dist>r) ? { x:dcx+dx/dist*r, y:dcy+dy/dist*r } : {x:x, y:y}  
  } 
  
  function dragged(d) {  
    //console.log(d.id);
    var obj = keepInCircle(d.dcx, d.dcy, d3.event.x, d3.event.y, 200)  
    d3.select(this)
      .attr("cx", obj.x)
      .attr("cy", obj.y);
  }
  
  function dragended(d) {
    d3.select(this)
      .attr("cx", d.dcx)
      .attr("cy", d.dcy)
      .classed("active", false);
  }
  
  </script>
dom free
  • 1,095
  • 1
  • 9
  • 8