3

I have this code:

var sets = [
    {sets: ['A'], size: 10},
    {sets: ['B'], size: 10},
    {sets: ['A','B'], size: 5}
];

var chart = venn.VennDiagram();
var div = d3.select("#venn").datum(sets).call(chart);

using excellent venn.js library, my venn diagram is drawn and works perfectly.

using this code:

            div.selectAll("g")
                .on("mouseover", function (d, i) {
                    // sort all the areas relative to the current item
                    venn.sortAreas(div, d);

                    // Display a tooltip with the current size
                    tooltip.transition().duration(400).style("opacity", .9);
                    tooltip.text(d.size + " items");

                    // highlight the current path
                    var selection = d3.select(this).transition("tooltip").duration(400);
                    selection.select("path")
                        .style("stroke-width", 3)
                        .style("fill-opacity", d.sets.length == 1 ? .4 : .1)
                        .style("stroke-opacity", 1)
                        .style("cursor", "pointer");
                })

                .on("mousemove", function () {
                    tooltip.style("left", (d3.event.pageX) + "px")
                           .style("top", (d3.event.pageY - 28) + "px");
                })

                .on("click", function (d, i) {
                    window.location.href = "/somepage"
                })

                .on("mouseout", function (d, i) {
                    tooltip.transition().duration(400).style("opacity", 0);
                    var selection = d3.select(this).transition("tooltip").duration(400);
                    selection.select("path")
                        .style("stroke-width", 1)
                        .style("fill-opacity", d.sets.length == 1 ? .25 : .0)
                        .style("stroke-opacity", 0);
                });

I'm able to add Click, mouseover,... functionality to my venn.

Here is the problem:

Adding functionality to Circles (Sets A or B) works fine.

Adding functionality to Intersection (Set A intersect Set B) works fine.

I need to add some functionality to Except Area (set A except set B)

This question helped a little: 2D Polygon Boolean Operations with D3.js SVG

But I had no luck making this work.

Tried finding out Except area using: clipperjs or Greiner-Hormann polygon clipping algorithm but couldn't make it work.

Update 1:

The code in this question is copied from venn.js sample: http://benfred.github.io/venn.js/examples/intersection_tooltip.html

Other samples: https://github.com/benfred/venn.js/

Community
  • 1
  • 1
Afshin Gh
  • 7,918
  • 2
  • 26
  • 43

1 Answers1

2

Perhaps you can do something like this....

Given 2 overlapping circles,

  • Find the two intersection points, and
  • Manually create a path that arcs from IP1 to IP2 along circle A and then from IP2 back to IP1 along circle B.

After that path is created (that covers A excluding B), you can style it however you want and add click events (etc.) to that SVG path element.

FIND INTERSECTION POINTS (IPs)

Circle-circle intersection points

var getIntersectionPoints = function(circleA, circleB){

  var x1 = circleA.cx,
      y1 = circleA.cy,
      r1 = circleA.r,
      x2 = circleB.cx,
      y2 = circleB.cy,
      r2 = circleB.r;

  var d = Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2)),
      a = (Math.pow(r1,2)-Math.pow(r2,2)+Math.pow(d,2))/(2*d),
      h = Math.sqrt(Math.pow(r1,2)-Math.pow(a,2));

  var MPx = x1 + a*(x2-x1)/d,
      MPy = y1 + a*(y2-y1)/d,
      IP1x = MPx + h*(y2-y1)/d,
      IP1y = MPy - h*(x2-x1)/d,
      IP2x = MPx - h*(y2-y1)/d,
      IP2y = MPy + h*(x2-x1)/d;

  return [{x:IP1x,y:IP1y},{x:IP2x,y:IP2y}]

}

MANUALLY CREATE PATH

var getExclusionPath = function(keepCircle, excludeCircle){

  IPs = getIntersectionPoints(keepCircle, excludeCircle);

  var start = `M ${IPs[0].x},${IPs[0].y}`,
      arc1 = `A ${keepCircle.r},${keepCircle.r},0,1,0,${IPs[1].x},${IPs[1].y}`,
      arc2 = `A ${excludeCircle.r},${excludeCircle.r},0,0,1,${IPs[0].x},${IPs[0].y}`,
      pathStr = start+' '+arc1+' '+arc2;

  return pathStr;

}

var height = 900;
    width  = 1600;
   
d3.select(".plot-div").append("svg")
  .attr("class", "plot-svg")
  .attr("width", "100%")
    .attr("viewBox", "0 0 1600 900")

var addCirc = function(circ, color){
  d3.select(".plot-svg").append("circle")
  .attr("cx", circ.cx)
  .attr("cy", circ.cy)
  .attr("r", circ.r)
  .attr("fill", color)
  .attr("opacity", "0.5")
}

var getIntersectionPoints = function(circleA, circleB){
  
  var x1 = circleA.cx,
    y1 = circleA.cy,
    r1 = circleA.r,
    x2 = circleB.cx,
    y2 = circleB.cy,
    r2 = circleB.r;
 
var d = Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2)),
    a = (Math.pow(r1,2)-Math.pow(r2,2)+Math.pow(d,2))/(2*d),
    h = Math.sqrt(Math.pow(r1,2)-Math.pow(a,2));

var MPx = x1 + a*(x2-x1)/d,
    MPy = y1 + a*(y2-y1)/d,
    IP1x = MPx + h*(y2-y1)/d,
    IP1y = MPy - h*(x2-x1)/d,
    IP2x = MPx - h*(y2-y1)/d,
    IP2y = MPy + h*(x2-x1)/d;
  
  return [{x:IP1x,y:IP1y},{x:IP2x,y:IP2y}]

}

var getExclusionPath = function(keepCircle, excludeCircle){

  IPs = getIntersectionPoints(keepCircle, excludeCircle);
  
  var start = `M ${IPs[0].x},${IPs[0].y}`,
      arc1 = `A ${keepCircle.r},${keepCircle.r},0,1,0,${IPs[1].x},${IPs[1].y}`,
      arc2 = `A ${excludeCircle.r},${excludeCircle.r},0,0,1,${IPs[0].x},${IPs[0].y}`,
      pathStr = start+' '+arc1+' '+arc2;
  
  return pathStr;
  
}



var circleA = {cx: 600, cy: 500, r: 400};
var circleB = {cx: 900, cy: 400, r: 300};

var pathStr = getExclusionPath(circleA, circleB)

addCirc(circleA, "steelblue");
addCirc(circleB, "darkseagreen");

d3.select(".plot-svg").append("text")
  .text("Hover over blue circle")
  .attr("font-size", 70)
  .attr("x", 30)
  .attr("y", 70)

d3.select(".plot-svg").append("path")
  .attr("class","exlPath")
  .attr("d", pathStr)
  .attr("stroke","steelblue")
  .attr("stroke-width","10")
  .attr("fill","white")
  .attr("opacity",0)
.plot-div{
  width: 50%;
  display: block;
  margin: auto;
}

.plot-svg {
  border-style: solid;
  border-width: 1px;
  border-color: green;
}

.exlPath:hover {
  opacity: 0.7;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

<div class="plot-div">

</div>

If you have more complex overlapping in your Venn diagrams (3+ region overlap) then this obviously gets more complicated, but I think you could still extend this approach for those situations.

Quick (sort of) note on to handle 3 set intersections ie. A∩B\C or A∩B∩C

There are 3 "levels" of overlap between A∩B and circle C...

  • Completely contained in C | Both AB IPs are in C
  • Partial overlap; C "cuts through" A∩B | Only one AB IP is in C
  • A∩B is completely outside C | No AB IPs are in C

Note: This is assuming C is not a subset of or fully contained by A or B -- otherwise, for example, both BC IPs could be contained in A

In total, you'll need 3 points to create the path for the 3 overlapping circles. The first 2 are along C where it "cuts through" A∩B. Those are...

  1. The BC intersection point contained in A
  2. The AC intersection point contained in B

For the 3rd point of the path, it depends if you want (i)A∩B∩C or (ii)A∩B\C...

  1. (i) A∩B∩C: The AB intersection point contained in C

    (ii) A∩B\C: The AB intersection point NOT contained in C

With those the points you can draw the path manually with the appropriate arcs.


Bonus -- Get ANY subsection for 2 circles

It's worth noting as well that you can get any subsection by choosing the right large-arc-flag and sweep-flag. Picked intelligently and you'll can get...

  1. Circle A (as path)
  2. Circle B (as path)
  3. A exclude B --in shown example
  4. B exclude A
  5. A union B
  6. A intersect B

... as well as a few more funky ones that won't match anything useful.

Some resources...

W3C site for elliptical curve commands

Good explanation for arc flags

Large-arc-flag: A value of 0 means to use the smaller arc, while a value of 1 means use the larger arc.

Sweep-flag: The sweep-flag determines whether to use an arc (0) or its reflection around the axis (1).

Community
  • 1
  • 1
Steve Ladavich
  • 3,472
  • 20
  • 27
  • Thanks a lot for your excellent answer. I'm going to award you both answer points & bounty points. With two circles this solution will work perfect, but what about 3 circles? What if we want [SetA + SetB - SetC]? What changes are needed to accomplish this? Could you kindly update your code or guide me in the right direction to calculate and highlight [(SetA + SetB) - SetC]? – Afshin Gh Jun 19 '16 at 15:52
  • Unfortunately, it gets trickier with 3+ circles. I'm considering writing some code to handle arbitrary set operations/combinations -- if I do, I'll update here. This may be akin to rewriting Venn.js with some extra features (I'm not too familiar with Venn.js) and may be a substantial effort. At the very least, I'll add a note about how to handle 3 circles/sets. – Steve Ladavich Jun 19 '16 at 16:56
  • Thanks Steve. I'm sure your answer will put me in the right path. – Afshin Gh Jun 19 '16 at 22:05