11

Hello I would like to know how it would be possible two make it that two circles change color when they overlap. Preferably the section that is overlapped would become white since its meant to represent sets.

var canvas = d3.select("canvas"),
    context = canvas.node().getContext("2d"),
    width = canvas.property("width"),
    height = canvas.property("height"),
    radius = 32;

var circles = d3.range(4).map(function(i) {
  return {
    index: i,
    x: Math.round(Math.random() * (width - radius * 2) + radius),
    y: Math.round(Math.random() * (height - radius * 2) + radius)
  };
});

var color = d3.scaleOrdinal()
    .range(d3.schemeCategory20);

render();

canvas.call(d3.drag()
    .subject(dragsubject)
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended)
    .on("start.render drag.render end.render", render));

function render() {
  context.clearRect(0, 0, width, height);
  for (var i = 0, n = circles.length, circle; i < n; ++i) {
    circle = circles[i];
    context.beginPath();
    context.moveTo(circle.x + radius, circle.y);
    context.arc(circle.x, circle.y, radius, 0, 2 * Math.PI);
    context.fillStyle = color(circle.index);
    context.fill();
    if (circle.active) {
      context.lineWidth = 2;
      context.stroke();
    }
  }
}

function dragsubject() {
  for (var i = circles.length - 1, circle, x, y; i >= 0; --i) {
    circle = circles[i];
    x = circle.x - d3.event.x;
    y = circle.y - d3.event.y;
    if (x * x + y * y < radius * radius) return circle;
  }
}

function dragstarted() {
  circles.splice(circles.indexOf(d3.event.subject), 1);
  circles.push(d3.event.subject);
  d3.event.subject.active = true;
}

function dragged() {
  d3.event.subject.x = d3.event.x;
  d3.event.subject.y = d3.event.y;
}

function dragended() {
  d3.event.subject.active = false;
}
<canvas width="800" height="500"></canvas>
<script src="//d3js.org/d3.v4.min.js"></script>

My ideal solution would be something that allow me to change the color of the overlapping section to another color to represent the intersection between 2 sets.

Thank you in advance

Edit: some updates have been made however Ive only found how to do the coloring for static elements instead of moving

var   x1 = 100,
      y1 = 100,
      x2 = 150,
      y2 = 150,
      r = 70;

    var svg = d3.select('svg')
      .append('svg')
      .attr('width', 500)
      .attr('height', 500);

    svg.append('circle')
      .attr('cx', x1)
      .attr('cy', y1)
      .attr('r', r)
      .style('fill', 'steelblue')
      .style("fill-opacity",0.5)
      .style("stroke","black");

    svg.append('circle')
      .attr('cx', x2)
      .attr('cy', y2)
      .attr('r', r)
      .style('fill', 'orange')
      .style("fill-opacity",0.5)
      .style("stroke","black");

    var interPoints = intersection(x1, y1, r, x2, y2, r);

    svg.append("g")
      .append("path")
      .attr("d", function() {
        return "M" + interPoints[0] + "," + interPoints[2] + "A" + r + "," + r +
          " 0 0,1 " + interPoints[1] + "," + interPoints[3]+ "A" + r + "," + r +
          " 0 0,1 " + interPoints[0] + "," + interPoints[2];
      })
      .style('fill', 'red')
      .style("fill-opacity",0.5)
      .style("stroke","black");


    function intersection(x0, y0, r0, x1, y1, r1) {
      var a, dx, dy, d, h, rx, ry;
      var x2, y2;

      /* dx and dy are the vertical and horizontal distances between
       * the circle centers.
       */
      dx = x1 - x0;
      dy = y1 - y0;

      /* Determine the straight-line distance between the centers. */
      d = Math.sqrt((dy * dy) + (dx * dx));

      /* Check for solvability. */
      if (d > (r0 + r1)) {
        /* no solution. circles do not intersect. */
        return false;
      }
      if (d < Math.abs(r0 - r1)) {
        /* no solution. one circle is contained in the other */
        return false;
      }

      /* 'point 2' is the point where the line through the circle
       * intersection points crosses the line between the circle
       * centers.  
       */

      /* Determine the distance from point 0 to point 2. */
      a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d);

      /* Determine the coordinates of point 2. */
      x2 = x0 + (dx * a / d);
      y2 = y0 + (dy * a / d);

      /* Determine the distance from point 2 to either of the
       * intersection points.
       */
      h = Math.sqrt((r0 * r0) - (a * a));

      /* Now determine the offsets of the intersection points from
       * point 2.
       */
      rx = -dy * (h / d);
      ry = dx * (h / d);

      /* Determine the absolute intersection points. */
      var xi = x2 + rx;
      var xi_prime = x2 - rx;
      var yi = y2 + ry;
      var yi_prime = y2 - ry;

      return [xi, xi_prime, yi, yi_prime];
    }
<script data-require="d3@3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<svg width="500" height="500"></svg>

^This works for statics

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height"),
    radius = 32;

var circles = d3.range(4).map(function() {
  return {
    x: Math.round(Math.random() * (width - radius * 2) + radius),
    y: Math.round(Math.random() * (height - radius * 2) + radius)
  };
});

var color = d3.scaleOrdinal()
    .range(d3.schemeCategory20);

svg.selectAll("circle")
  .data(circles)
  .enter().append("circle")
   .style("fill-opacity",0.3)
    .style("stroke","black")
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; })
    .attr("r", 60)
    .style("fill", function(d, i) { return color(i); })
    .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

function dragstarted(d) {
  d3.select(this).raise().classed("active", true);
}

function dragged(d) {
  d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}

function dragended(d) {
  d3.select(this).classed("active", false);
}
<svg width="500" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>

^This is my moving circles that I would like to add said effect on.

Is there any way to combine the two codes to achieve this ?

Thanks again

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
Cormanno
  • 165
  • 1
  • 8
  • Maybe a way to achieve that could be with the `context.globalCompositeOperation` property (https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation). But I'm not sure that you can customize your own color for the overlapping section. – JulCh Dec 13 '17 at 09:25
  • It doesn't seem to work with the moving circles and I am also using svg as opposed to canvas. I have kinda found a way for it to work while static but not while moving. – Cormanno Dec 13 '17 at 10:39

2 Answers2

1

You can use the intersection function of your static approach (second snippet) inside the dragged function of your dynamic approach (third snippet).

First of all, let's create 2 groups, so the "intersection" path will always be in front of the circles:

var g1 = svg.append("g");
var g2 = svg.append("g");

Now to the important part.

Inside the dragged function, get the position of the other (non-dragged) circle:

var otherCircle = circles.filter(function(e, j) {
    return i !== j;
}).datum();

If you have more than two circles you'll have to refactor this, but my demo below has just two circles, so let's move on.

Then, check if they overlap:

Math.hypot(d.x - otherCircle.x, d.y - otherCircle.y) < 2 * radius

If they do, call intersection, and set the path's d attribute:

var interPoints = intersection(d.x, d.y, radius, otherCircle.x, otherCircle.y, radius);
path.attr("d", function() {
  return "M" + interPoints[0] + "," + interPoints[2] + "A" + radius + "," + radius +
    " 0 0,1 " + interPoints[1] + "," + interPoints[3] + "A" + radius + "," + radius +
    " 0 0,1 " + interPoints[0] + "," + interPoints[2];
})

If they don't, erase the path:

path.attr("d", null)

Here is the working demo:

var svg = d3.select("svg"),
  width = +svg.attr("width"),
  height = +svg.attr("height"),
  radius = 60;

var data = d3.range(2).map(function(d, i) {
  return {
    x: i ? 200 : 400,
    y: 150
  };
});

var g1 = svg.append("g");
var g2 = svg.append("g");

var color = d3.scaleOrdinal()
  .range(d3.schemeCategory10);

var circles = g1.selectAll("circle")
  .data(data)
  .enter().append("circle")
  .style("fill-opacity", 0.3)
  .style("stroke", "black")
  .attr("cx", function(d) {
    return d.x;
  })
  .attr("cy", function(d) {
    return d.y;
  })
  .attr("r", radius)
  .style("fill", function(d, i) {
    return color(i);
  })
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));

var path = g2.append("path")
  .style("fill", "white")
  .style("stroke", "black")
  .attr("d", null);

function dragstarted(d) {
  d3.select(this).raise().classed("active", true);
}

function dragged(d, i) {
  d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
  var otherCircle = circles.filter(function(e, j) {
    return i !== j;
  }).datum();
  if (Math.hypot(d.x - otherCircle.x, d.y - otherCircle.y) < 2 * radius) {
    var interPoints = intersection(d.x, d.y, radius, otherCircle.x, otherCircle.y, radius);
    path.attr("d", function() {
      return "M" + interPoints[0] + "," + interPoints[2] + "A" + radius + "," + radius +
        " 0 0,1 " + interPoints[1] + "," + interPoints[3] + "A" + radius + "," + radius +
        " 0 0,1 " + interPoints[0] + "," + interPoints[2];
    })
  } else {
    path.attr("d", null)
  }
}

function dragended(d) {
  d3.select(this).classed("active", false);
}

function intersection(x0, y0, r0, x1, y1, r1) {
  var a, dx, dy, d, h, rx, ry;
  var x2, y2;

  /* dx and dy are the vertical and horizontal distances between
   * the circle centers.
   */
  dx = x1 - x0;
  dy = y1 - y0;

  /* Determine the straight-line distance between the centers. */
  d = Math.sqrt((dy * dy) + (dx * dx));

  /* Check for solvability. */
  if (d > (r0 + r1)) {
    /* no solution. circles do not intersect. */
    return false;
  }
  if (d < Math.abs(r0 - r1)) {
    /* no solution. one circle is contained in the other */
    return false;
  }

  /* 'point 2' is the point where the line through the circle
   * intersection points crosses the line between the circle
   * centers.  
   */

  /* Determine the distance from point 0 to point 2. */
  a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d);

  /* Determine the coordinates of point 2. */
  x2 = x0 + (dx * a / d);
  y2 = y0 + (dy * a / d);

  /* Determine the distance from point 2 to either of the
   * intersection points.
   */
  h = Math.sqrt((r0 * r0) - (a * a));

  /* Now determine the offsets of the intersection points from
   * point 2.
   */
  rx = -dy * (h / d);
  ry = dx * (h / d);

  /* Determine the absolute intersection points. */
  var xi = x2 + rx;
  var xi_prime = x2 - rx;
  var yi = y2 + ry;
  var yi_prime = y2 - ry;

  return [xi, xi_prime, yi, yi_prime];
}
svg {
  background-color: wheat;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="600" height="300"></svg>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
0

SVG mix-blend-mode

SVG mix-blend-mode hover from screen to normal

This isn't exactly what you're looking for since it doesn't let you programmatically control the color of intersecting segments, but CSS mix-blend-mode is a very simple solution I've used with d3. I've tried to accomplish the same thing but ran into performance problems calculating intersections on animating large datasets. Compatibility may be a concern if this needs to work in IE/ Edge, but otherwise most modes are supported in Chrome, Firefox, and Safari (even mobile).

Here's a good guide with examples on d3 as well and a simplified Codepen snippet.

Otherwise it seems like you've already found D3.js - detect intersection area. To get that to work with drag, you'll need to write the calculations to determine which circles are overlapping, then calculate their intersection area.

Tom
  • 6,947
  • 7
  • 46
  • 76