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.
Related
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).
I'm trying to imitate the following effects:
Orginal version V3:
https://bl.ocks.org/mbostock/7881887
Converted to V4:
https://bl.ocks.org/lydiawawa/1fe3c80d35e046c1636663442f34680b/86d1bda1dabb7f3a6d11cb1a16053564078ed964
An example used dataset:
https://jsfiddle.net/hf998do7/1/
This is what I have so far :
https://blockbuilder.org/lydiawawa/0899a02cc86f2274f52e27064bc86500
I want to make a bubble graph that shows clusters of Race, the size of the bubbles are assigned by BMI. The dots will not overlap (collide). There is a toggle control on the left top corner; when it is turned to the right, bubbles cluster into groups separated by Race, when it is turned to the left the dots combine and mix together and centers on the canvas ( in a large circle).
I tried to code for a clustered bubbles, but with the dataset I have there are too many jitters, takes a while for the split to stop. So I found Bostock's versions of clustered bubbles, they are intended to reduce jitters.
What I struggled the most is to fit my dataset with Bostock's version III layout, and I accidentally used the wrong version of forceCluster(alpha) code because I was looking through his different versions (I updated the code in the following section).
In Bostock's original code, he created his own bubbles by defining the following variable, nodes. He used a custom formula to define the distribution of nodes. However, in my case, I will be using my own dataset, bubbleChart.csv with a toggle that combines and splits the cluster.
Bostock's custom defined nodes:
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
r = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {
cluster: i,
radius: r,
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
});
He then defined force function to reduce jitter:
var force = d3.layout.force()
.nodes(nodes) // not sure how to redefine this
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
Code for clustering (updated):
function forceCluster(alpha) {
return function(d) {
var cluster = clusters[d.Race];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.Race + cluster.Race;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
I'm having trouble to fit everything together in particularly to adapt to my own dataset and would like to have some help. Thank you!
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'm receiving all distances between a random number of points in a 2 dimensional coordinate system.
How can I visualize this as coordinates on a map in my browser?
In case there are many solutions I just want to see the first possible one that my algorithm can come up with.
So here's an extremely easy example:
PointCount = 3
Distances:
0-1 = 2
0-2 = 4
1-2 = 2
Does anyone know an easy way (existing solution/framework maybe) to do it using whatever is out there to make it easier to implement?
I was thinking maybe using the html canvas element for drawing, but I don't know how to create an algorithm that could come up with possible coordinates for those points.
The above example is simplified -
Real distance values could look like this:
(0) (1) (2) (3)
(0) 0 2344 3333 10000
(1) 0 3566 10333
(2) 0 12520
I'm not sure this is relevant for SO, but anyway...
The way to do this is quite simply to place the points one by one using the data:
Pick a random location for the first point (let's say it's 0,0).
The second point is on a circle with radius d(0,1) with the first point as its center, so you can pick any point on the circle. Let's pick (d(0,1),0).
The third point is at the intersection of a circle with radius d(0,2) and center point 1, and a circle with radius d(1,2) and center point 2. You will get either 0, 1, 2 or an infinity of solutions. If the data comes from real points, 0 shouldn't happen. 1 and infinity are edge cases, but you should still handle them. Pick any of the solutions.
The fourth point is at the intersection of 3 circles. Unless you're very unlucky (but you should account for it), there should be only one solution.
Continue like this until all points have been placed.
Note that this doesn't mean you'll get the exact locations of the original points: you can have any combination of a translation (the choice of your first point), rotation (the choice of your second point) and symmetry (the choice of your third point) making the difference.
A quick and dirty implementation (not handling quite a few cases, and tested very little):
function distance(p1, p2) {
return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
}
// adapted from https://stackoverflow.com/a/12221389/3527940
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, yi],
[xi_prime, yi_prime]
];
}
function generateData(nbPoints) {
var i, j, k;
var originalPoints = [];
for (i = 0; i < nbPoints; i++) {
originalPoints.push([Math.random() * 20000 - 10000, Math.random() * 20000 - 10000]);
}
var data = [];
var distances;
for (i = 0; i < nbPoints; i++) {
distances = [];
for (j = 0; j < i; j++) {
distances.push(distance(originalPoints[i], originalPoints[j]));
}
data.push(distances);
}
//console.log("original points", originalPoints);
//console.log("distance data", data);
return data;
}
function findPointsForDistances(data, threshold) {
var points = [];
var solutions;
var solutions1, solutions2;
var point;
var i, j, k;
if (!threshold)
threshold = 0.01;
// First point, arbitrarily set at 0,0
points.push([0, 0]);
// Second point, arbitrarily set at d(0,1),0
points.push([data[1][0], 0]);
// Third point, intersection of two circles, pick any solution
solutions = intersection(
points[0][0], points[0][1], data[2][0],
points[1][0], points[1][1], data[2][1]);
//console.log("possible solutions for point 3", solutions);
points.push(solutions[0]);
//console.log("solution for points 1, 2 and 3", points);
found = true;
// Subsequent points, intersections of n-1 circles, use first two to find 2 solutions,
// the 3rd to pick one of the two
// then use others to check it's valid
for (i = 3; i < data.length; i++) {
// distances to points 1 and 2 give two circles and two possible solutions
solutions = intersection(
points[0][0], points[0][1], data[i][0],
points[1][0], points[1][1], data[i][1]);
//console.log("possible solutions for point " + (i + 1), solutions);
// try to find which solution is compatible with distance to point 3
found = false;
for (j = 0; j < 2; j++) {
if (Math.abs(distance(solutions[j], points[2]) - data[i][2]) <= threshold) {
point = solutions[j];
found = true;
break;
}
}
if (!found) {
console.log("could not find solution for point " + (i + 1));
console.log("distance data", data);
console.log("solution for points 1, 2 and 3", points);
console.log("possible solutions for point " + (i + 1), solutions);
console.log("distances to point 3",
distance(solutions[0], points[2]),
distance(solutions[1], points[2]),
data[i][2]
);
break;
}
// We have found a solution, we need to check it's valid
for (j = 3; j < i; j++) {
if (Math.abs(distance(point, points[j]) - data[i][j]) > threshold) {
console.log("Could not verify solution", point, "for point " + (i + 1) + " against distance to point " + (j + 1));
found = false;
break;
}
}
if (!found) {
console.log("stopping");
break;
}
points.push(point);
}
if (found) {
//console.log("complete solution", points);
return points;
}
}
console.log(findPointsForDistances([
[],
[2344],
[3333, 3566],
[10000, 10333, 12520],
]));
console.log(findPointsForDistances([
[],
[2],
[4, 2],
]));
console.log(findPointsForDistances([
[],
[4000],
[5000, 3000],
[3000, 5000, 4000]
]));
console.log(findPointsForDistances([
[],
[2928],
[4938, 3437],
[10557, 10726, 13535]
]));
var nbPoints, i;
for (nbPoints = 4; nbPoints < 8; nbPoints++) {
for (i = 0; i < 10; i++) {
console.log(findPointsForDistances(generateData(nbPoints)));
}
}
Fiddle here: https://jsfiddle.net/jacquesc/82aqmpnb/15/
Minimum working example. Remember that in canvas coordinates, the y value is inverted but you could do something like:
y = canvasHeight - y
If you also have negative points then if would take a little bit of extra work. Also it may be helpful in that case to draw lines and tick marks to visualize the axis.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let scale = 10;
let radius = 10;
function point(x, y) {
ctx.fillRect(x*scale, y*scale, radius, radius);
}
// test
point(10, 15);
point(20, 8);
<html>
<body>
<canvas id="canvas" width=1000 height=1000></canvas>
</body>
</html>
There are plenty of libraries out there.
chartist.js is easy to use and responsive JavaS cript library. I used it last year for basic charts after trying many others but it was the only one that scaling easily in different screen sizes.
chartJS is another better looking library.
And you can use html5 canvas it's easy and fun but it will take time especially in scaling.
To scale and position, you should use the minimum and maximum values for x and y.
Good luck
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