D3 curved labels in the center of arc - javascript
I've been able to construct labeled donut chart just like in the following fiddle:
http://jsfiddle.net/MX7JC/9/
But now I'm trying to place the label in the middle of each arc and to span them along the arc (curve the label to follow each arc). To do that I've been thinking of putting the svg:text along svg:textPath using the d3.svg.line.radial function.
Then I stumbled upon the following fiddle:
http://jsfiddle.net/Wexcode/CrDUy/
However I'm having difficulty to tie the var arcs (the one having actual data) from the former fiddle with the var line from the latter fiddle as the latter fiddle uses the d3.range function as the data.
I've been doing trial-and-error for hours but nothing works. Does anyone know how the d3.svg.line.radial works together with the d3.svg.arc?
The d3.svg.line.radial function constructs a series of cubic Bezier curves (not arcs) between multiple points in an array based on input polar coordinates (radius and angle) for each point.
(The example you linked to appears to draw a circle, but only because it breaks the circle down to many tightly spaced points -- try using 5 points instead of 50, and you'll see that the shape of the curve isn't a real circle.)
The d3.svg.arc function contstructs a shape consisting of two con-centric arcs and the straight lines connecting them, based on values for innerRadius, outerRadius, startAngle and endAngle.
Both methods specify angles in radians starting from "12 o'clock" (vertical pointing up). However, there are a couple difficulties in getting the radial line function to work with the arc data objects.
The first problem is that the line generator expects to be passed an array of multiple points, not a single object. In order to ger around that, you'll have to set the datum of the path element to be an array of the arc group's object repeated twice, once for the start and once for the end of the arc, and then use a function in i to determine whether the startAngle or the endAngle should be used for the angle value of each point.
Here's a variation of your fiddle creating those paths. I haven't bothered getting the text to run along the path, I'm just drawing the paths in black:
http://jsfiddle.net/MX7JC/688/
Now you see the second problem: if only given two points, the line generator will just create a straight line between them.
See simple curve example: http://jsfiddle.net/4VnHn/5/
In order to get any kind of curve with the default line generators, you need to add additional points to act as control points, and change the line interpolate method to an "open" option so that the end control points aren't drawn. I found that making the start and end control points 45 degrees beyond the start and end points of the curve (around the circle) created a curve that was acceptably similar to an arc in my simple example.
See better simple curve example: http://jsfiddle.net/4VnHn/6/
For your visualization, the curves generator now has to be passed the data object repeated four times in an array, and the angle accessor is now going to need a switch statement to figure out the different points: http://jsfiddle.net/MX7JC/689/
The results are acceptable for the small donut segments, but not for the ones that are wider than 45 degrees themselves -- in these cases, the control points end up so far around the around the circle that they throw off the curve completely. The curve generator doesn't know anything about the circle, it's just trying to smoothly connect the points to show a trend from one to the next.
A better solution is to actually draw an arc, using the arc notation for SVG paths. The arc generator uses arc notation, but it creates the full two-dimensional shape. To create arcs with a line generator, you're going to need a custom line interpolator function which you can then pass to the line generator's interpolate method.
The line generator will execute the custom line interpolator function, passing in an array of points that have already been converted from polar coordinates to x,y coordinates. From there you need to define the arc equation. Because an arc function also need to know the radius for the arc, I use a nested function -- the outside function accepts the radius as a parameter and returns the function that will accept the points array as a parameter:
function arcInterpolator(r) {
//creates a line interpolator function
//which will draw an arc of radius `r`
//between successive polar coordinate points on the line
return function(points) {
//the function must return a path definition string
//that can be appended after a "M" command
var allCommands = [];
var startAngle; //save the angle of the previous point
//in order to allow comparisons to determine
//if this is large arc or not, clockwise or not
points.forEach(function(point, i) {
//the points passed in by the line generator
//will be two-element arrays of the form [x,y]
//we also need to know the angle:
var angle = Math.atan2(point[0], point[1]);
//console.log("from", startAngle, "to", angle);
var command;
if (i) command = ["A", //draw an arc from the previous point to this point
r, //x-radius
r, //y-radius (same as x-radius for a circular arc)
0, //angle of ellipse (not relevant for circular arc)
+(Math.abs(angle - startAngle) > Math.PI),
//large arc flag,
//1 if the angle change is greater than 180degrees
// (pi radians),
//0 otherwise
+(angle < startAngle), //sweep flag, draws the arc clockwise
point[0], //x-coordinate of new point
point[1] //y-coordinate of new point
];
else command = point; //i = 0, first point of curve
startAngle = angle;
allCommands.push( command.join(" ") );
//convert to a string and add to the command list
});
return allCommands.join(" ");
};
}
Live example: http://jsfiddle.net/4VnHn/8/
To get it to work with your donut graph, I started with the version above that was producing straight lines, and changed the interpolate parameter of the line generator to use my custom function. The only additional change I had to make was to add an extra check to make sure none of the angles on the graph ended up more than 360 degrees (which I'm sure was just a rounding issue on the last arc segment, but was causing my function to draw the final arc the entire way around the circle, backwards):
var curveFunction = d3.svg.line.radial()
.interpolate( arcInterpolator(r-45) )
.tension(0)
.radius(r-45)
.angle(function(d, i) {
return Math.min(
i? d.endAngle : d.startAngle,
Math.PI*2
);
//if i is 1 (true), this is the end of the curve,
//if i is 0 (false), this is the start of the curve
});
Live example: http://jsfiddle.net/MX7JC/690/
Finally, to use these curves as text paths:
set the curve to have no stroke and no fill;
give each curve a unique id value based on your data categories
(for your example, you could use the donut label plus the data label to come up with something like "textcurve-Agg-Intl");
add a <textPath> element for each label;
set the text paths' xlink:href attribute to be # plus the same unique id value for that data
I thought of a different approach. It's slightly roundabout way to do it, but requires a lot less custom code.
Instead of creating a custom line interpolator to draw the arc, use a d3 arc generator to create the curve definition for an entire pie segment, and then use regular expressions to extract the curve definition for just the outside curve of the pie.
Simplified example here: http://jsfiddle.net/4VnHn/10/
Example with the donut chart here: http://jsfiddle.net/MX7JC/691/
Key code:
var textArc = d3.svg.arc().outerRadius(r-45); //to generate the arcs for the text
textCurves.attr("d", function(d) {
var pie = textArc(d); //get the path code for the entire pie piece
var justArc = /[Mm][\d\.\-e,\s]+[Aa][\d\.\-e,\s]+/;
//regex that matches a move statement followed by an arc statement
return justArc.exec(pie)[0];
//execute regular expression and extract matched part of string
});
The r-45 is just halfway between the inner and outer radii of the donuts. The [\d\.\-e,\s]+ part of the regular expression matches digits, periods, negative signs, exponent indicators ('e'), commas or whitespace, but not any of the other letters which signify a different type of path command. I think the rest is pretty self-explanatory.
Related
Warp an SVG in JavaScript into an arc
I saw a d3 helper function which can create an svg arc: making an arc in d3.js Is there a way to bend an existing svg into an arc in d3.js? If not, how can it be done in JavaScript? The initial image could be a square with negative space inside that makes a silhouette of something like a cat. I included a drawing of what I want. I eventually want to make more and create a ring, and then concentric rings of smaller transformations.
I think conformal mapping is the way to go. As an example, I have started with the original square source and the circularly arranged (and appropriately warped) squares using conformal mapping. See the Circular Image and the Corresponding Square Image. Note, however, that I first converted the SVG source into a png image, and then applied conformal mapping transformation to each pixel in the png image. If anyone has a method of mapping entirely in the SVG domain [where the source uses only straight lines and bezier curves], pl. let us know! I am not able to provide the code -- it is too complex and uses proprietary libraries). Here is a way to do it. In order to compute the conformal mapping, I used a complex algebra library. JEP is one example. For each pixel in the OUTPUT image, you set Z = (x, y) where (x, y) are the coordinates of the pixel and Z is a complex variable with real part x and imaginary part y. Then you compute NEWZ = f(Z) where f is any function you want. for circle, I used an expression such as "(ln(Z*1)/pi)*6-9" where the log (ln) provides circularity, the 6 implies 6 bent squares in a circle and the -9 is a scaling factor to center and place the image. From the (X, Y) values of the NEWZ, you look up the old image to find the pixel to place in the new image. When the (X, Y) values are outside the size of the old image, I just use tiling of the old image to determine the pixel. When X and Y are fractional (non-integer) you average the neighbors in a weighted way.
Drawing a line segment using cocos2djs
I want to draw a line segment using cocos2d-js. Suppose I have two points cc.p(50,50); and cc.p(200,200); , how do I connect these two points using a line? I've looked around and cc.DrawNode(); is not having a drawLine() method though it has a drawDot() method. So I thought I should go for cc.DrawingPrimitiveCanvas(renderContext); but I can't seem to get it working. I am only starting to learn cocos2d-js. It would be very helpful if you can show me how to draw a simple line, be it using DrawNode() or DrawingPrimitiveCanvas().
I got the answer. var line = new cc.DrawNode(); line.drawSegment(cc.p(50,50), cc.p(200,200),2); I should've been looking for the 'drawSegment' method within 'DrawNode'. Here's the usage: drawSegment(from, to, lineWidth, color) //draw a segment with a radius and color Parameters: {cc.Point} from, {cc.Point} to, {Number} lineWidth, {cc.Color} color
Cubic Bezier Curve between two points on a sphere in three.js
I'm letting the user click on two points on a sphere and I would then like to draw a line between the two points along the surface of the sphere (basically on the great circle). I've been able to get the coordinates of the two selected points and draw a QuadraticBezierCurve3 between the points, but I need to be using CubicBezierCurve3. The problem is is that I have no clue how to find the two control points. Part of the issue is everything I find is for circular arcs and only deals with [x,y] coordinates (whereas I'm working with [x,y,z]). I found this other question which I used to get a somewhat-working solution using QuadraticBezierCurve3. I've found numerous other pages with math/code like this, this, and this, but I really just don't know what to apply. Something else I came across mentioned the tangents (to the selected points), their intersection, and their midpoints. But again, I'm unsure of how to do that in 3D space (since the tangent can go in more than one direction, i.e. a plane). An example of my code: http://jsfiddle.net/GhB82/ To draw the line, I'm using: function drawLine(point) { var middle = [(pointA['x'] + pointB['x']) / 2, (pointA['y'] + pointB['y']) / 2, (pointA['z'] + pointB['z']) / 2]; var curve = new THREE.QuadraticBezierCurve3(new THREE.Vector3(pointA['x'], pointA['y'], pointA['z']), new THREE.Vector3(middle[0], middle[1], middle[2]), new THREE.Vector3(pointB['x'], pointB['y'], pointB['z'])); var path = new THREE.CurvePath(); path.add(curve); var curveMaterial = new THREE.LineBasicMaterial({ color: 0xFF0000 }); curvedLine = new THREE.Line(path.createPointsGeometry(20), curveMaterial); scene.add(curvedLine); } Where pointA and pointB are arrays containing the [x,y,z] coordinates of the selected points on the sphere. I need to change the QuadraticBezierCurve3 to CubicBezierCurve3, but again, I'm really at a loss on finding those control points.
I have a description on how to fit cubic curves to circular arcs over at http://pomax.github.io/bezierinfo/#circles_cubic, the 3D case is essentially the same in that you need to find out the (great) circular cross-section your two points form on the sphere, and then build the cubic Bezier section along that circle. Downside: Unless your arc is less than or equal to roughly a quarter circle, one curve is not going to be enough, you'll need two or more. You can't actually model true circular curves with Bezier curves, so using cubic instead of quadratic just means you can approximate a longer arc segment before it starts to look horribly off. So on a completely different solution note: if you have an arc command available, much better to use that than to roll your own (and if three.js doesn't support them, definitely worth filing a feature request for, I'd think)
Interconnected curved lines
Given a series of JSON co-ordinates typically in the format: {from: {x:0, y:0}, to: {x:0, y:10}, ...} I would like to draw a series of straight dotted paths which are connected with simple, fixed radius rounded corners. I have been looking at Slope Intercept Form to calculate the points along the straight line but I am a little perplexed as to the approach for calcualting the points along the (Bezier?) curves. e.g. I want to draw curves between p1 and p2 and p3 and p4. Despite what the poor mockup might imply I am happy for the corners to be a fixed radius e.g. 10px I would like to abstract out the drawing logic and therefore am seeking a generalised approach to returning a JavaScript point array which I can then render in a number of ways (hence I am avoiding using any inbuilt functions provided by SVG, Canvas etc).
What you want is a cubic bezier curve. http://www.blackpawn.com/texts/splines/ Look at the first applet on this page. If A is p1, D is p2, the direction A-B is line 1's angle and the direction C-D is line 2's angle you can see how this gives you the properties you need - it starts at angle 1 and ends at angle 2 and is flush with the points. So, to get your points C and D, one way to do this would be to take the line segment 1, copy it, place it starting at p1 - and say where the new line ends is B, and similar with line segment 2 and p2 for D. (And you could do things like have a factor that multiplies into the copied line segments' distance to make the curves stick out more or less... etc) Then just do the math :) http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves And once you have your equation for the curve, step through it with a delta t of the desired precision (e.g. every 0.1 of t, every 0.01...) and spit out every pair of points on the curve as a line segment.
How to Draw line in 3D rectangle based on x,y and z?
I would like draw 3D points represented in image to 3D rectangle. Any idea how could I represent these in x,y and z axis Here projection type is orthographic. Thanks
Okay. Let's look at a simple example of what you are trying to accomplish it, and why this is such a complicated problem. First, lets look a some projection functions. You need a way to mathematically describe how to transform a 3D (or higher dimensional) point into a 2D space (your monitor), or a projection. The simpiest to understand is a very simple dimetric projection. Something like: x' = x + z/2; y' = y + z/4; What does this mean? Well, x' is you x coordinate 2D projection: for every unit you move backwards in space, the projection will move that point half that many units to the right. And y' represents that same projection for your y coordinate: for every unit you move backwards in space, the projection will move that point a quarter unit up. So a point at [0,0,0] will get projected to a 2d point of [0,0]. A point at [0,0,4] will get projected to a 2d point of [2,1]. Implemented in JavaScript, it would look something like this: // Dimetric projection functions var dimetricTx = function(x,y,z) { return x + z/2; }; var dimetricTy = function(x,y,z) { return y + z/4; }; Once you have these projection functions -- or ways to translate from 3D space into 2D space -- you can use them to start draw your image. A simple example of that using js canvas. First, some context stuff: var c = document.getElementById("cnvs"); var ctx = c.getContext("2d"); Now, lets make a little helper to draw a 3D point: var drawPoint = (function(ctx,tx,ty, size) { return function(p) { size = size || 3; // Draw "point" ctx.save(); ctx.fillStyle="#f00"; ctx.translate(tx.apply(undefined, p), ty.apply(undefined,p)); ctx.beginPath(); ctx.arc(0,0,size,0,Math.PI*2); ctx.fill(); ctx.restore(); }; })(ctx,dimetricTx,dimetricTy); This is pretty simple function, we are injecting the canvas context as ctx, as well as our tx and ty functions, which in this case our the dimetric functions we saw earlier. And now a polygon drawer: var drawPoly = (function(ctx,tx,ty) { return function() { var args = Array.prototype.slice.call(arguments, 0); // Begin the path ctx.beginPath(); // Move to the first point var p = args.pop(); if(p) { ctx.moveTo(tx.apply(undefined, p), ty.apply(undefined, p)); } // Draw to the next point while((p = args.pop()) !== undefined) { ctx.lineTo(tx.apply(undefined, p), ty.apply(undefined, p)); } ctx.closePath(); ctx.stroke(); }; })(ctx, dimetricTx, dimetricTy); With those two functions, you could effectively draw the kind of graph you are looking for. For example: // The array of points var points = [ // [x,y,z] [20,30,40], [100,70,110], [30,30,75] ]; (function(width, height, depth, points) { var c = document.getElementById("cnvs"); var ctx = c.getContext("2d"); // Set some context ctx.save(); ctx.scale(1,-1); ctx.translate(0,-c.height); ctx.save(); // Move our graph ctx.translate(100,20); // Draw the "container" ctx.strokeStyle="#999"; drawPoly([0,0,depth],[0,height,depth],[width,height,depth],[width,0,depth]); drawPoly([0,0,0],[0,0,depth],[0,height,depth],[0,height,0]); drawPoly([width,0,0],[width,0,depth],[width,height,depth],[width,height,0]); drawPoly([0,0,0],[0,height,0],[width,height,0],[width,0,0]); ctx.stroke(); // Draw the points for(var i=0;i<points.length;i++) { drawPoint(points[i]); } })(150,100,150,points); However, you should now be able to start to see some of the complexity of your actual question emerge. Namely, you asked about rotation, in this example we are using an extremely simple projection (our dimetric projection) which doesn't take much other than an oversimplified relationship between depth and its influences on x,y position. As the projections become more complex, you need to know more about your relationship/orientation in 3D space in order to create a reasonable 2D projection. A working example of the above code can be found here. The example also includes isometric projection functions that can be swapped out for the dimetric ones to see how that changes the way the graph looks. It also does some different visualization stuff that I didn't include here, like drawing "shadows" to help "visualize" the actual orientation -- the limitations of 3D to 2D projections. It's complicated, and even a superficial discussion is kind of beyond the scope of this stackoverflow. I recommend you read more into the mathematics behind 3D, there are plenty of resources, both online and in print form. Once you have a more solid understanding of the basics of how the math works then return here if you have a specific implementation question about it.
What you want to do is impossible to do using the method you've stated - this is because a box - when rotated in 3 dimensions won't look anything like that diagram of yours. It will also vary based on the type of projection you need. You can, however get started using three.js which is a 3D drawing library for Javascript. Hope this helps.
How to Draw 3D Rectangle? posted in: Parallelogram | updated on: 14 Sep, 2012 To sketch 3 - Dimensional Rectangle means we are dealing with the figures which are different from 2 – D figures, which would need 3 axes to represent them. So, how to draw 3D rectangle? To start with, first make two lines, one vertical and another horizontal in the middle of the paper such that they represent a “t” letter of English. This is what we need to draw for temporary use and will be removed later after the construction of the 3 – D rectangle is complete. Next we draw a Square whose measure of each side is 1 inch. Square must be perfect in Geometry so that 90 degree angles that are formed at respective corners are exact in measure. Now starting from upper right corner of the square we draw a line segment that will be stretched to a measure of 2 inches in the direction at an angle of 45 degrees. Similarly, we repeat the procedure by drawing another Line Segment from the upper left corner of the square and stretching it to 2 inches length in the direction at an angle of 45 degrees. These 2 line segments are considered to be the diagonals with respect to the horizontal line that we drew temporarily in starting. Also these lines will be parallel to each other. Next we draw a line that joins the end Point of these two diagonals. Next starting from the very right of the 2 inch diagonal end point, draw a line of measure 1 inch that is supposed to be perpendicular to the temporary horizontal line. Next we need to join the lower left corner of the square with end point of the last 1’’ line we drew in 4th step and finally we get our 3 - D rectangular. Now we can erase our initial “t”. This 3- D rectangle resembles a Cuboid.