Draw multiple circles using d3.js in a 3x3 format - javascript

I am trying to draw 9 circles in a 3x3 format using d3.js .
Here is my script:-
<script src="//d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="//ajax.aspnetcdn.com/ajax/jquery/jquery-1.9.0.min.js"></script>
<div class="barChart"></div>
<div class="circles"></div>
<style>
</style>
<script>
$(document).ready(function () {
circles();
$(".circles").show();
function circles() {
var svg = d3.select(".circles").append("svg");
var data = ["Z","Z","Z","Z","Z","Z","Z","Z","Z"];
var groups = svg.selectAll("g")
.data(data).attr("width",100).attr("height",100)
.enter()
.append("g");
groups.attr("transform", function(d, i) {
var x = 100;
console.log(i);
var y = 50 * i + 100 ;
return "translate(" + [x,y] + ")";
});
var circles = groups.append("circle")
.attr({cx: function(d,i){
return 0;
},
cy: function(d,i){
return 0;}})
.attr("r", "20")
.attr("fill", "red")
.style("stroke-width","2px");
var label = groups.append("text")
.text(function(d){
return d;
})
.style({
"alignment-baseline": "middle",
"text-anchor": "middle",
"font-family":"Arial",
"font-size":"30",
"fill":"white"
});
}
});
But , I am just getting some half circles.
And also , I tried to have only one value in the data and tried to run the circles() in a loop but I got circles in a straight line with lot of spacing between them
for (var i = 0; i <=8 ;i++){
circles();
}
And
var groups = svg.selectAll("g")
.data("Z").attr("width",100).attr("height",100)
.enter()
.append("g");
If I follow the second method, I got circles in a straight line and with lot of spacing.
So how to get the circles like in the figure and with less spacing ?
Can anyone please help me

Your transform function is positioning your elements in a line instead of a grid. If you log out what you are translating you will see whats happening. i.e.
console.log("translate(" + [x,y] + "");
/* OUTPUTS
translate(100,100)
translate(100,150)
translate(100,200)
translate(100,250)
translate(100,300)
translate(100,350)
translate(100,400)
translate(100,450)
translate(100,500)
translate(100,100)
*/
They are being stacked one on top of the other vertically.
You can modify the transform function by changing the following lines:
var x = 100 + (50* Math.floor(i/3));
var y = 50 * (i%3) + 20 ;
Finally, the SVG container is clipping your drawing so you are seeing only half of anything below 150px.
You can modify the width as follows:
var svg = d3.select(".circles")
.append("svg")
.attr({"height": '300px'});

The simplest way to fix your code is to modify the groups transform
groups.attr("transform", function(d, i) {
var x = (i % 3) * 100 + 200; // x varies between 200 and 500
var y = 50 * Math.floor(i / 3) + 100 ; // y varies between 100 and 250
return "translate(" + [x,y] + ")";
});
Here, i will loop between 0 and 8 because you have 8 elements in your data variable, in the x assigment, i%3 is worth 0,1,2,0,1,2,0,1,2, and in the y assignement, Math.floor(i / 3) is worth 0,0,0,1,1,1,2,2,2 so you basically get a 2D grid :
0,0 1,0 2,0
1,0 1,1 2,1
2,0 2,1 2,2

Related

How to get points from a D3 SVG bezier curve with given step size?

So I have a D3 SVG bezier curve like this :
I want to get X, Y coordinates of points with given step from the D3 path like so:
So to get an array of coordinate pairs with equal steps. If possible not recalculating bezier curve. How to do such a thing in D3js?
With the help of a bisection function from here you probably want something like this:
const
svg = d3.select("svg"),
width = 512,
height = 200;
const data = [];
const curve = "curveBasis";
var walk = function() {
for (let i = 0, v = 2; i < 50; ++i) {
v += Math.random() - 0.5;
v = Math.max(Math.min(v, 4), 0);
data.push({step: i, value: v});
}
}
walk();
walkX = d3.scaleLinear()
.domain([0, 49])
.range([10, width - 10]);
walkY = d3.scaleLinear()
.domain([0, 4])
.range([200 - 10, 10]);
const line = d3.line()
.curve(d3[curve])
.x(d => walkX(d.step))
.y(d => walkY(d.value));
svg
.append("path")
.attr("id", "svgpath")
.attr("d", line(data))
.attr("fill", "none")
.attr("stroke", "black");
var svgpath = document.getElementById("svgpath");
var findYatXbyBisection = function(x, path, error){
var length_end = path.getTotalLength(),
length_start = 0,
point = path.getPointAtLength((length_end + length_start) / 2), // get the middle point
bisection_iterations_max = 50,
bisection_iterations = 0;
error = error || 0.01;
while (x < point.x - error || x > point.x + error) {
// get the middle point
point = path.getPointAtLength((length_end + length_start) / 2);
if (x < point.x) {
length_end = (length_start + length_end)/2;
} else {
length_start = (length_start + length_end)/2;
}
// Increase iteration
if (bisection_iterations_max < ++ bisection_iterations)
break;
}
return point.y
}
for (let i = 0; i < data.length; ++i) {
console.log(findYatXbyBisection(walkX(i), svgpath, 0.01).toFixed(4));
}
<html>
<head>
<meta charset="utf-8" />
<title>SVG Line</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.1/d3.min.js" integrity="sha512-COTaPOlz12cG4fSfcBsxZsjauBAyldqp+8FQUM/dZHm+ts/jR4AFoJhCqxy8K10Jrf3pojfsbq7fAPTb1XaVkg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<svg id="chart" width="512" height="200">
</svg>
Do note that Y values returned are SVG coordinates so they start at 0 from the top of page. Check the range function used in the walkY function to refresh how you have to reverse the values for typical line and bar charts in D3.js.
And of course instead of logging to the console you can push the values to your custom array and use different interval value e.g. 1/n of the total line (path) width instead of 1/50 I've used (for 50 data points).

Offset Line stroke-weight d3.js

I'm using d3.js to plot a highway network over a map SVG. I'd like to be able to vary the stroke-weight of the line to illustrate demand based on a value.
Highway links are define as one way, so for example a two way road would have two overlapping line elements (with separate id's). I can use stroke-weight to edit the thickness of the line based on a variable (as below), but on a two way road, the larger of the two stroke weights will always cover the smaller rendering it invisible.
Is there an easy way to offset a line by half its stroke-weight to the left hand side of the direction the line is drawn? (direction denoted by x1,y1 x2,y2)
d3.csv("links.csv", function (error, data) {
d3.select("#lines").selectAll("line")
.data(data)
.enter()
.append("line")
.each(function (d) {
d.p1 = projection([d.lng1, d.lat1]);
d.p2 = projection([d.lng2, d.lat2]);
})
.attr("x1", function (d) { return d.p1[0]; })
.attr("y1", function (d) { return d.p1[1]; })
.attr("x2", function (d) { return d.p2[0]; })
.attr("y2", function (d) { return d.p2[1]; })
.on('mouseover', tip_link.show)
.on('mouseout', tip_link.hide)
.style("stroke", "black")
.style("stroke-width", lineweight)
});
One option would be to just create new start/end points when drawing your lines and use those:
var offset = function(start,destination,distance) {
// find angle of line
var dx = destination[0] - start[0];
var dy = destination[1] - start[1];
var angle = Math.atan2(dy,dx);
// offset them:
var newStart = [
start[0] + Math.sin(angle-Math.PI)*distance,
start[1] + Math.cos(angle)*distance
];
var newDestination = [
destination[0] + Math.sin(angle-Math.PI)*distance,
destination[1] + Math.cos(angle)*distance
];
// return the new start/end points
return [newStart,newDestination]
}
This function takes two points and offsets them by a particular amount given the angle between the two points. Negative values shift to the other side, swapping the start and destination points will shift to the other side.
In action, this looks like, with the original line in black:
var offset = function(start,destination,distance) {
// find angle of line
var dx = destination[0] - start[0];
var dy = destination[1] - start[1];
var angle = Math.atan2(dy,dx);
// offset them:
var newStart = [
start[0] + Math.sin(angle-Math.PI)*distance,
start[1] + Math.cos(angle)*distance
];
var newDestination = [
destination[0] + Math.sin(angle-Math.PI)*distance,
destination[1] + Math.cos(angle)*distance
];
// return the new start/end points
return [newStart,newDestination]
}
var line = [
[10,10],
[200,100]
];
var svg = d3.select("svg");
// To avoid repetition:
function draw(selection) {
selection.attr("x1",function(d) { return d[0][0]; })
.attr("x2",function(d) { return d[1][0]; })
.attr("y1",function(d) { return d[0][1]; })
.attr("y2",function(d) { return d[1][1]; })
}
svg.append("line")
.datum(line)
.call(draw)
.attr("stroke","black")
.attr("stroke-width",1)
svg.append("line")
.datum(offset(...line,6))
.call(draw)
.attr("stroke","orange")
.attr("stroke-width",10)
svg.append("line")
.datum(offset(...line,-4))
.call(draw)
.attr("stroke","steelblue")
.attr("stroke-width",5)
<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
You will need to adapt this to your data structure, and it requires twice as many lines as before, because you aren't using stroke width, your using lines. This is advantageous if you wanted to use canvas.

Circle approximation that is a regular polygon with N corners

I have to implement the idea of a circle approximation that is a regular polygon with N corners, whereas N is defined by the user.
For example, if N=3 I would have a triangle. With n=5 I would a shape that starts resembling a circle. As I increase N, I would get closer and closer to the shape of a circle.
This idea it's very similar to what was asked and answered on the on the follwing question/solution:
Draw regular polygons inscribed in a circle , however, they used raphael.js and not D3.js.
What I tried to do:
var vis = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 667);
var svg = d3.select('svg');
var originX = 200;
var originY = 200;
var outerCircleRadius = 60;
var outerCircle = svg.append("circle").attr({
cx: originX,
cy: originY,
r: outerCircleRadius,
fill: "none",
stroke: "black"
});
var chairWidth = 10;
var chairOriginX = originX + ((outerCircleRadius) * Math.sin(0));
var chairOriginY = originY - ((outerCircleRadius) * Math.cos(0));
var chair = svg.append("rect").attr({
x: chairOriginX - (chairWidth / 2),
y: chairOriginY - (chairWidth / 2),
width: chairWidth,
opacity: 1,
height: 20,
fill: "none",
stroke: "blue"
});
var n_number = 5
var n_angles = 360/n_number
var angle_start=0;
var angle_next;
console.log(chair.node().getBBox().x);
console.log(chair.node().getBBox().y);
chair.attr("transform", "rotate(" + (angle_start+n_angles+n_angles) + ", 200, 200)");
var circle = svg.append("circle")
.attr("cx", 195)
.attr("cy", 135)
.attr("r", 50)
.attr("fill", "red");
var chairOriginX2 = originX + ((outerCircleRadius) * Math.sin(0));
var chairOriginY2 = originY - ((outerCircleRadius) * Math.cos(0));
var chair2 = svg.append("rect").attr({
x: chairOriginX2 - (chairWidth / 2),
y: chairOriginY2 - (chairWidth / 2),
width: chairWidth,
opacity: 1,
height: 20,
fill: "none",
stroke: "blue"
});
console.log(chair2.node().getBBox().x);
console.log(chair2.node().getBBox().y);
My idea, that did not work, was trying to create a circle ("outerCircle") which I would slide within the circunference ("chair.attr("transform"...") of the circle, based on N, obtaining several different (x,y) coordinates.
Then, I would feed (x,y) coordinates to a polygon.
I believe that my approach for this problem is wrong. Also, the part that I got stuck is that I am not being able to keep sliding whitin the circunference and storing each different (x,y) coordinate. I tried "console.log(chair2.node().getBBox().x);" but it is always storing the same coordinate, which it is of the origin.
I've simplified your code for clarity. To get the x of a point on a circle you use the Math.cos(angle) and for the y you use the Math.sin(angle). This was your error. Now you can change the value of the n_number
var SVG_NS = 'http://www.w3.org/2000/svg';
var originX = 200;
var originY = 200;
var outerCircleRadius = 60;
var polygon = document.createElementNS(SVG_NS, 'polygon');
svg.appendChild(polygon);
let points="";
var n_number = 5;
var n_angles = 2*Math.PI/n_number
// building the value of the `points` attribute for the polygon
for(let i = 0; i < n_number; i++){
let x = originX + outerCircleRadius * Math.cos(i*n_angles);
let y = originY + outerCircleRadius * Math.sin(i*n_angles);
points += ` ${x},${y} `;
}
// setting the value of the points attribute of the polygon
polygon.setAttributeNS(null,"points",points)
svg{border:1px solid;width:90vh;}
polygon{fill: none;
stroke: blue}
<svg id="svg" viewBox = "100 100 200 200" >
<circle cx="200" cy="200" r="60" fill="none" stroke="black" />
</svg>
This is another demo where I'm using an input type range to change the n_numbervariable
var SVG_NS = 'http://www.w3.org/2000/svg';
var originX = 200;
var originY = 200;
var outerCircleRadius = 60;
var polygon = document.createElementNS(SVG_NS, 'polygon');
svg.appendChild(polygon);
let points="";
var n_number = 5;
setPoints(n_number);
theRange.addEventListener("input", ()=>{
n_number = theRange.value;
setPoints(n_number)
});
function setPoints(n_number){
var n_angles = 2*Math.PI/n_number;
points = ""
// building the value of the `points` attribute for the polygon
for(let i = 0; i < n_number; i++){
let x = originX + outerCircleRadius * Math.cos(i*n_angles);
let y = originY + outerCircleRadius * Math.sin(i*n_angles);
points += ` ${x},${y} `;
}
// setting the value of the points attribute of the polygon
polygon.setAttributeNS(null,"points",points);
}
svg{border:1px solid; width:90vh;}
polygon{fill: none;
stroke: blue}
<p><input type="range" min="3" max="50" value="5" id="theRange" /></p>
<svg id="svg" viewBox = "100 100 200 200" >
<circle cx="200" cy="200" r="60" fill="none" stroke="black" />
</svg>
The answer provided by enxaneta is perfectly fine and is certainly the classical approach to this. However, I often favor letting the browser do the trigonometry instead of doing it on my own. Typical examples include my answer to "Complex circle diagram" or the one to "SVG marker - can I set length and angle?". I am not even sure if they outperform the more classical ones but I like them for their simplicity nonetheless.
My solution focuses on the SVGGeometryElement and its methods .getTotalLength() and .getPointAtLength(). Since the SVGCircleElement interface extends that interface those methods are available for an SVG circle having the following meanings:
.getTotalLength(): The circumference of the circle.
.getPointAtLength(): The point in x-/y-coordinates on the circle at the given length. The measurement per definition begins at the 3 o'clock position and progresses clockwise.
Given these explanations it becomes apparent that you can divide the circle's total length, i.e. its circumference, by the number of points for your approximation. This gives you the step distance along the circle to the next point. By summing up these distances you can use the second method to obtain the x-/y-coordinates for each point.
The coding could be done along the following lines:
// Calculate step length as circumference / number of points.
const step = circleElement.getTotalLength() / count;
// Build an array of points on the circle.
const data = Array.from({length: count}, (_, i) => {
const point = circleElement.getPointAtLength(i * step); // Get coordinates of next point.
return `${point.x},${point.y}`;
});
polygon.attr("points", data.join(" "));
Slick and easy! No trigonometry involved.
Finally, a complete working demo:
// Just setup, not related to problem.
const svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
const circle = svg.append("circle")
.attr("cx", "150")
.attr("cy", "150")
.attr("r", "100")
.attr("fill", "none")
.attr("stroke", "black");
const polygon = svg.append("polygon")
.attr("fill", "none")
.attr("stroke", "blue");
const circleElement = circle.node();
const ranger = d3.select("#ranger").on("input", update);
const label = d3.select("label");
// This function contains all the relevant logic.
function update() {
let count = ranger.node().value;
label.text(count);
// Calculate step length as circumference / number of points.
const step = circleElement.getTotalLength() / count;
// Build an array of all points on the circle.
const data = Array.from({length: count}, (_, i) => {
const point = circleElement.getPointAtLength(i * step); // Get coordinates of next point.
return `${point.x},${point.y}`;
});
polygon.attr("points", data.join(" "));
}
update();
<script src="https://d3js.org/d3.v5.js"></script>
<p>
<input id="ranger" type="range" min="3" max="15" value="5">
<label for="ranger"></label>
</p>

d3.js reverse transition does not work

I am working on horizontal segment bar chart. I want to make it so that the bar chart will animate the colour transition between individual segments depending on the value that is generated randomly every few seconds.
I also have a text box that at the moment says "hello" and it is moving simultaneously with the transition.It works only in one direction from left to right.
I cannot make the colour transition between segments and translating the text box from left to right. From left to right I mean that the number generated last time is greater than the currently generated number. The bar chart should turn off the segments from right to left. But it does it from left to right also the text box is acting weirdly when it comes to reverse translation.
I am also getting this error: g attribute transform: Expected transform function, "null". In my code I want to make a transition on my valueLabel and I think the way I am using is not correct. Despite this error the code gets executed.
My fiddle code is here
Many thanks for suggestions
var configObject = {
svgWidth : 1000,
svgHeight : 1000,
minValue : 1,
maxValue : 100,
midRange : 50,
highRange : 75,
numberOfSegments : 50
};
//define variables
var newValue;
var gaugeValue = configObject.minValue - 1;
var mySegmentMappingScale;
var rectArray=[];
//define svg
var svg = d3.select("body").append("svg")
.attr("width", configObject.svgWidth)
.attr("height", configObject.svgHeight)
.append("g")
.attr("transform", 'translate(' + configObject.svgWidth/2 + ',' + configObject.svgHeight/2 + ')');
//var myG=svg.append('g');
var valueLabel= svg.append("text")
.attr('x',0)
.attr('y', (configObject.svgHeight/13)+15)
.text("hello");
var backgroundRect=svg.append("rect")
.attr("fill", "black")
.attr("x",0)
.attr("y", 0)
.attr("width", (configObject.svgWidth/3))
.attr("height", configObject.svgHeight/13);
for(i = 0; i <= configObject.numberOfSegments; i++){
var myRect=svg.append("rect")
.attr("fill", "#2D2D2D")
.attr("x",i * ((configObject.svgWidth/3)/configObject.numberOfSegments))
.attr("y", 0)
.attr("id","rect"+i)
.attr("width", ((configObject.svgWidth/3)/configObject.numberOfSegments)-3)
.attr("height", configObject.svgHeight/13);
rectArray.push(myRect);
}
//define scale
function setmySegmentMappingScale(){
var domainArray = [];
var x=0;
for(i = configObject.minValue; i <= configObject.maxValue+1; i = i + (configObject.maxValue - configObject.minValue)/configObject.numberOfSegments){
if(Math.floor(i) != domainArray[x-1]){
var temp=Math.floor(i);
domainArray.push(Math.floor(i));
x++;
}
}
var rangeArray = [];
for(i = 0; i <= configObject.numberOfSegments+1; i++){// <=
rangeArray.push(i);
}
mySegmentMappingScale = d3.scale.threshold().domain(domainArray).range(rangeArray);
}
//generate random number
function generate(){
var randomNumber = Math.random() * (configObject.maxValue - configObject.minValue) + configObject.minValue;
newValue = Math.floor(randomNumber);
setmySegmentMappingScale();
animateSVG();
}
function animateSVG(){
var previousSegment = mySegmentMappingScale(gaugeValue) -1;
var newSegment = mySegmentMappingScale(newValue) -1;
if(previousSegment <= -1 && newSegment > -1){
for(i = 0; i <= newSegment; i++){
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolate( "#2D2D2D","red"); });
valueLabel
.transition().ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * ((configObject.svgWidth/3)/configObject.numberOfSegments)+((configObject.svgWidth/3)/configObject.numberOfSegments)) + "," + 0 + ")")
}
}
else if(newSegment > previousSegment){
for(i = previousSegment; i <= newSegment; i++){
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolate( "#2D2D2D","red"); });
//console.log(temp);
valueLabel
.transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * ((configObject.svgWidth/3)/configObject.numberOfSegments)+((configObject.svgWidth/3)/configObject.numberOfSegments)) + "," + 0 + ")")
}
}
else if(newSegment < previousSegment){
for(i = previousSegment; i > newSegment; i--){
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolate( "red","#2D2D2D"); });
valueLabel
.transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * ((configObject.svgWidth/3)/configObject.numberOfSegments)-((configObject.svgWidth/3)/configObject.numberOfSegments)) + "," + 0 + ")")
}
}
gaugeValue = newValue;
}
setInterval(function() {
generate()
}, 8000);
The problem is just the delay.
When newSegment > previousSegment, you set the delay like this:
.delay(function(d){return i * 90})
Which makes sense, because i is an increasing variable. But, when newSegment < previousSegment the same math doesn't work anymore: i is a decreasing variable, and the delay has to increase as i decreases, not the other way around.
This is what you need:
.delay(function(d){return Math.abs(i -previousSegment)*90})
Here is your updated fiddle: https://jsfiddle.net/b7usw2nr/

venn.js selecting Set[A] except Set[B]

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/
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...
The BC intersection point contained in A
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...
(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...
Circle A (as path)
Circle B (as path)
A exclude B --in shown example
B exclude A
A union B
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).

Categories