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).
Related
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.
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>
I am using d3 to create a diagram to try and speed up a nearest nabour search within a function that plots points on a plane.
Is there a way to add points directly to the diagram so I can add the points within a while loop instead of re-drawing the entire voronoi?
var svg = d3.select("svg")
var distance = function(pa, pb) {
var x = pa[0] - pb[0],
y = pa[1] - pb[1]
return Math.sqrt((x * x) + (y * y))
}
var scatterCircle = function(point, radius, quantity, proximity, margin) {
var x1 = point[0] - radius,
y1 = point[1] - radius,
inner = radius * margin,
array = [
[500, 500]
]
//should be declaring diagram here and addings points below//
while (array.length < quantity) {
//constructing new diagram each loop to test function, needs add to diagram function//
var newpoly = d3.voronoi()(array),
x = x1 + (radius * 2 * Math.random()),
y = y1 + (radius * 2 * Math.random()),
ii = newpoly.find(x, y).index
var d = distance(array[ii], [x, y]),
e = distance([x, y], point)
if (e < inner) {
if (d > proximity) {
array.push([x, y])
}
}
}
return array
}
var test = scatterCircle([500, 500], 500, 1500, 10, 0.9)
var o = 0
while (o < test.length) {
svg.append("circle")
.attr("cx", test[o][0])
.attr("cy", test[o][1])
.attr("r", 1)
o++
}
<script src="https://d3js.org/d3.v4.js"></script>
<svg width="1000" height="1000">
I am no expert in d3.js but I will share what I found out. The implemented algorithm for Voronoi diagrams is Fortune's algorithm. This is the classical algorithm to compute a Voronoi diagram. Inserting a new point is neither part of this algorithm nor of the function set documented for d3.js. But you are correct, inserting one new site does not require to redraw the whole diagram in theory.
You use the Voronoi diagram for NNS (nearest neighbour search). You could also use a 2d-tree to accomplish NNS. There insertion and removal is easier. A quick search revealed two implementations in javascript: kd-tree-javascript and kd-tree-js.
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
I'm trying to grasp how the functions in Donut3D.js -> http://plnkr.co/edit/g5kgAPCHMlFWKjljUc3j?p=preview handle the inserted data:
Above all, where is it set that the data's startAngle is set at 0 degrees?
I want to change it to 45º, then to 135º, 225º and 315º (look at the image above).
I've located this function:
Donut3D.draw = function(id, data, x /*center x*/, y/*center y*/,
rx/*radius x*/, ry/*radius y*/, h/*height*/, ir/*inner radius*/){
var _data = d3.layout.pie().sort(null).value(function(d) {return d.value;})(data);
var slices = d3.select("#"+id).append("g").attr("transform", "translate(" + x + "," + y + ")")
.attr("class", "slices");
slices.selectAll(".innerSlice").data(_data).enter().append("path").attr("class", "innerSlice")
.style("fill", function(d) {
return d3.hsl(d.data.color).darker(0.7); })
.attr("d",function(d){
return pieInner(d, rx+0.5,ry+0.5, h, ir);})
.each(function(d){this._current=d;});
slices.selectAll(".topSlice").data(_data).enter().append("path").attr("class", "topSlice")
.style("fill", function(d) {
return d.data.color; })
.style("stroke", function(d) {
return d.data.color; })
.attr("d",function(d){
return pieTop(d, rx, ry, ir);})
.each(function(d){this._current=d;});
slices.selectAll(".outerSlice").data(_data).enter().append("path").attr("class", "outerSlice")
.style("fill", function(d) {
return d3.hsl(d.data.color).darker(0.7); })
.attr("d",function(d){
return pieOuter(d, rx-.5,ry-.5, h);})
.each(function(d){this._current=d;});
slices.selectAll(".percent").data(_data).enter().append("text").attr("class", "percent")
.attr("x",function(d){
return 0.6*rx*Math.cos(0.5*(d.startAngle+d.endAngle));})
.attr("y",function(d){
return 0.6*ry*Math.sin(0.5*(d.startAngle+d.endAngle));})
.text(getPercent).each(function(d){this._current=d;});
}
and tried to insert an arc such as :
var arc = d3.svg.arc().outerRadius(r)
.startAngle(function(d) { return d.startAngle + Math.PI/2; })
.endAngle(function(d) { return d.endAngle + Math.PI/2; });
but it doesn't produce the desired effects.
EDIT 1
The first answer helped in rotating the inner pie, by changing:
var _data = d3.layout.pie().sort(null).value(function(d) {
return d.value;
})(data);
to
var _data = d3.layout.pie()
.startAngle(45*Math.PI/180)
.endAngle(405*Math.PI/180).sort(null).value(function(d) {
return d.value;
})(data);
the problem is that now the outer pie gets broken -> http://plnkr.co/edit/g5kgAPCHMlFWKjljUc3j?p=preview
I guess the solution has something to do with the function function pieOuter(d, rx, ry, h ) and the two startAngle and endAngle variables, but they work in apparently unpredictable ways.
Thank you
I know that Pie Charts are bad, especially if in 3D; but this work
is part of my thesis where my job is actually demonstrate how
PieCharts are Bad! I want to rotate this PieChart in order to show how
if the 3D pie Slice is positioned at the top the data shows as less
important, or more important if positioned at the bottom. So a 'Evil
Journalist' could alter the visual perception of data by simply
inclinating and rotating the PieChart!
Here's a corrected function which allows rotation.
First, modify function signature to include rotate variable:
Donut3D.draw = function(id, data, x /*center x*/ , y /*center y*/ ,
rx /*radius x*/ , ry /*radius y*/ , h /*height*/ , ir /*inner radius*/, rotate /* start angle for first slice IN DEGREES */ ) {
In the draw function, modify angles. Instead of screwing with pie angles, I'd do it to the data directly:
_data.forEach(function(d,i){
d.startAngle += rotate * Math.PI/180; //<-- convert to radians
d.endAngle += rotate * Math.PI/180;
});
Then you need to correct the pieOuter function to fix the drawing artifacts:
function pieOuter(d, rx, ry, h) {
var startAngle = d.startAngle,
endAngle = d.endAngle;
var sx = rx * Math.cos(startAngle),
sy = ry * Math.sin(startAngle),
ex = rx * Math.cos(endAngle),
ey = ry * Math.sin(endAngle);
// both the start and end y values are above
// the middle of the pie, don't bother drawing anything
if (ey < 0 && sy < 0)
return "M0,0";
// the end is above the pie, fix the points
if (ey < 0){
ey = 0;
ex = -rx;
}
// the beginning is above the pie, fix the points.
if (sy < 0){
sy = 0;
sx = rx;
}
var ret = [];
ret.push("M", sx, h + sy, "A", rx, ry, "0 0 1", ex, h + ey, "L", ex, ey, "A", rx, ry, "0 0 0", sx, sy, "z");
return ret.join(" ");
}
Here's the full code
Changing the default start angle
Donut3D users d3's pie layout function here, which has a default startAngle of 0.
If you want to change the start angle, you should modify donut3d.js.
In the first place, you should certainly avoid to use 3d pie/donut charts, if you care about usability and readability of your visualizations - explained here.
Fixing bottom corner layout
The endAngle you are using is not correct, causing the "light blue" slice to overlap the "blue" one. Should be 405 (i.e. 45 + 360) instead of 415.
var _data = d3.layout.pie()
.startAngle(45*Math.PI/180)
.endAngle(405*Math.PI/180)
Then, the "pieOuter" angles calculation should be updated to behave correctly. The arc which doesn't work is the one where endAngle > 2 * PI, and the angle computation should be updated for it.
This does the trick (don't ask me why):
// fix right-side outer shape
if (d.endAngle > 2 * Math.PI) {
startAngle = Math.PI / 120
endAngle = Math.PI/4
}
demo: http://plnkr.co/edit/wmPnS9XVyQcrNu4WLa0D?p=preview