Arash Howaida
Arash Howaida

Reputation: 2617

D3 polygonContains() for simple rect

I have used polygonContains(polygon, [x,y]) in the past and I notice that the polygon parameter takes coordinates. I do not have that kind of data structure, I have more of a "pre-fab" SVG shape if you will. Specifically SVG rects.

Question: What are some options for implementing the polygongContains() call when you actually only have SVG rects?

My initial thought was to reverse engineer the SVG rect by doing this at rect creating:

.on('click', function(d) {
    var containing_rect = d3.select(this);
    console.log(containing_rect)
    console.log(containing_rect.x1)
    var points_contained = d3.selectAll('circle').filter(function(d) {
        return !d3.polygonContains(containing_rect, [x(d.x),y(d.y)]);
    })
})

But containing_rect.x1 appeared as undefined in the log, which leads me to believe my reverse engineer approach will not be able to get the end points of the rect.

Perhaps there is a better way to interface with polygonContains() in terms of passing arguments to have an SVG rect be used as a 4 sided bounding polygon (that the call expects).

Upvotes: 1

Views: 1283

Answers (2)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

You can create your own function to check if the circle is inside a rectangle.

For instance, this function I just wrote:

function containing(container, point) {
    var border = container.node().getBBox();
    var coordinates = {
        left: border.x,
        right: border.x + border.width,
        top: border.y,
        bottom: border.y + border.height
    };
    if (point.attr("cx") > coordinates.left && 
        point.attr("cx") < coordinates.right && 
        point.attr("cy") > coordinates.top && 
        point.attr("cy") < coordinates.bottom) {
        return true
    } else {
        return false
    }
}

It takes two arguments, container (the rectangle) and points (the circles), both as D3 selections.

Here is a demo of the function, I'm creating 100 circles and positioning them randomly. Those that fall outside the rectangle are coloured yellow, those that fall inside are coloured blue:

var w = 400,
  h = 200;

var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);

var rectangle = svg.append("rect")
  .attr("x", 100)
  .attr("y", 50)
  .attr("width", 150)
  .attr("height", 100)
  .attr("fill", "white")
  .attr("stroke", "teal");

var circles = svg.selectAll("schrodinger")
  .data(d3.range(100))
  .enter()
  .append("circle")
  .attr("cx", function(d) {
    return Math.random() * w
  })
  .attr("cy", function(d) {
    return Math.random() * h
  })
  .attr("r", 4);

circles.each(function() {
  var circle = d3.select(this);
  circle.attr("fill", function() {
    return containing(rectangle, circle) ? "royalblue" : "goldenrod";
  })
});

function containing(container, point) {
  var border = container.node().getBBox();
  var coordinates = {
    left: border.x,
    right: border.x + border.width,
    top: border.y,
    bottom: border.y + border.height
  };
  if (point.attr("cx") > coordinates.left && point.attr("cx") < coordinates.right && point.attr("cy") > coordinates.top && point.attr("cy") < coordinates.bottom) {
    return true
  } else {
    return false
  }
}
<script src="https://d3js.org/d3.v4.min.js"></script>

Bonus: using a D3 drag, you can check the position of the circles inside the drag function. Here is a demo, drag the rectangle:

var w = 400,
  h = 200;

var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);

var rectangle = svg.append("rect")
  .datum({
    x: 100,
    y: 50
  })
  .attr("x", function(d) {
    return d.x
  })
  .attr("y", function(d) {
    return d.y
  })
  .attr("width", 150)
  .attr("height", 100)
  .attr("fill", "white")
  .attr("stroke", "teal")
  .call(d3.drag().on("drag", dragged));

var circles = svg.selectAll("schrodinger")
  .data(d3.range(100))
  .enter()
  .append("circle")
  .attr("cx", function(d) {
    return Math.random() * w
  })
  .attr("cy", function(d) {
    return Math.random() * h
  })
  .attr("r", 4)
  .attr("pointer-events", "none");

circles.each(function() {
  var circle = d3.select(this);
  circle.attr("fill", function() {
    return containing(rectangle, circle) ? "royalblue" : "goldenrod";
  })
});

function containing(container, point) {
  var border = container.node().getBBox();
  var coordinates = {
    left: border.x,
    right: border.x + border.width,
    top: border.y,
    bottom: border.y + border.height
  };
  if (point.attr("cx") > coordinates.left && point.attr("cx") < coordinates.right && point.attr("cy") > coordinates.top && point.attr("cy") < coordinates.bottom) {
    return true
  } else {
    return false
  }
}

function dragged(d) {
  d3.select(this).attr("x", d.x = d3.event.x).attr("y", d.y = d3.event.y);
  circles.each(function() {
    var circle = d3.select(this);
    circle.attr("fill", function() {
      return containing(rectangle, circle)  ? "royalblue" : "goldenrod";
    })
  });
}
<script src="https://d3js.org/d3.v4.min.js"></script>

EDIT: In case you want to use polygonContains, we first get the rectangle's corners the same way we did in the last snippet, and populate an array named polygon. Then, we use it in your selection:

circles.each(function() {
    var points = [+d3.select(this).attr("cx"), +d3.select(this).attr("cy")]
    d3.select(this).attr("fill", function() {
        return d3.polygonContains(polygon, points) ? "green" : "red";
    })
});

Here is a demo:

var w = 400,
  h = 200;

var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);

var rectangle = svg.append("rect")
  .datum({
    x: 100,
    y: 50
  })
  .attr("x", function(d) {
    return d.x
  })
  .attr("y", function(d) {
    return d.y
  })
  .attr("width", 150)
  .attr("height", 100)
  .attr("fill", "white")
  .attr("stroke", "teal");

var border = rectangle.node().getBBox();
var corners = {
  left: border.x,
  right: border.x + border.width,
  top: border.y,
  bottom: border.y + border.height
};
var polygon = [
  [corners.left, corners.top],
  [corners.right, corners.top],
  [corners.right, corners.bottom],
  [corners.left, corners.bottom]
];

var circles = svg.selectAll("schrodinger")
  .data(d3.range(100))
  .enter()
  .append("circle")
  .attr("cx", function(d) {
    return Math.random() * w
  })
  .attr("cy", function(d) {
    return Math.random() * h
  })
  .attr("r", 3);

circles.each(function() {
  var points = [+d3.select(this).attr("cx"), +d3.select(this).attr("cy")]
  d3.select(this).attr("fill", function() {
    return d3.polygonContains(polygon, points) ? "green" : "red";
  })
});
<script src="https://d3js.org/d3.v4.min.js"></script>

Upvotes: 2

Daniela Mogini
Daniela Mogini

Reputation: 539

You can use

containing_rect.attr("x")
containing_rect.attr("y")

to retrive x and y coordinate of the top-left point of the rect and

containing_rect.attr("width")
containing_rect.attr("height")

to retrieve width and height. Then you'll do the maths ;)

Upvotes: -1

Related Questions