D3 Arc With Chevron Shaped End - javascript

I've drawn an arc using D3.js which by default has square shaped ends.
var arc = d3.arc()
.innerRadius(0)
.outerRadius(100)
.startAngle(0)
.endAngle(Math.PI);
d3.selectAll('svg')
.append('path')
.attr('d', function() {
return arc();
});
How can I draw an arc with a chevron shape on one end of it.

I think I understand what you are looking for, so I'll give it a go:
As you probably guess from the d3.js documentation, d3.arc() does not have the methods needed to make a point at one end. Padding and rounded corners are applied on both ends, and I can't see how they would work to form a point at both ends let alone one.
Two solutions come to mind (and there are probably many that I can't even conceive of)
Lop off the end of each arc, based on its end angle, and append a triangle or other similar shape (alternatively, apply some sort of mask to trim the end into a point)
Attempt to rework d3.arc() to your needs, taking up the invitation to develop/refine d3 in a modular fashion.
Personally, I think option one is probably much less clean and probably harder to design. Option two should be doable, and with this encouragement to dive in and make modules:
Small files are nice, but modularity is also about making D3 more fun. Microlibraries are easier to understand, develop and test. They make it easier for new people to get involved and contribute. They reduce the distinction between a “core module” and a “plugin”, and increase the pace of development in D3 features. (https://github.com/d3/d3/blob/master/CHANGES.md)
I thought I'd give this a go.
I've put together an attempt that might be a start for a chevron tipped arc module based on the d3.arc() function.
The rounded corners portion of the d3.arc() function in the d3-shape.js module is likely the best place to look as it shows modifications to the arc ends. The portions of the module that modify the arc, in the event of rounded corners, look like:
context.arc(t0.cx, t0.cy, rc1, Math.atan2(t0.y01, t0.x01), Math.atan2(t0.y11, t0.x11), !cw);
context.arc(0, 0, r1, Math.atan2(t0.cy + t0.y11, t0.cx + t0.x11), Math.atan2(t1.cy + t1.y11, t1.cx + t1.x11), !cw);
context.arc(t1.cx, t1.cy, rc1, Math.atan2(t1.y11, t1.x11), Math.atan2(t1.y01, t1.x01), !cw);
The outer edge is handled first (and shown above). The first line is the rounding on the rear outside corner, the third line is the rounding on the forward outside corner. Simply removing the third line allows for a pointed arc (if you remove it from the inside edge too). Then the remaining challenge is making the other end of the arc flat, which I did by using the start angle and the inner & outer radii to find the corners of the arc to create a flat end.
The end result was something like:
// get tail coordinate (outer)
var tailOuter = {};
tailOuter.x = Math.cos(a0) * r1; // a0 = starting angle
tailOuter.y = Math.sin(a0) * r1; // r1 = outer radius
context.moveTo(tailOuter.x, tailOuter.y);
context.arc(0, 0, r1, Math.atan2(t0.cy + t0.y11, t0.cx + t0.x11), Math.atan2(t1.cy + t1.y11, t1.cx + t1.x11), !cw);
I've put together a quick and dirty module that takes the d3.arc() function and creates a d3.cheveronArc() function instead. It's a gutted modification to d3.arc() and has only four methods (inner/outerRadius(),start/endAngle()). It has no means to check for parameters that will likely cause misbehavior (eg: chevron is longer than the arc). It is merely a proof of concept, though I am happy with how it looks for a rather quick attempt:
As you might notice, the inner most circle has an odd shape near its tail, small inner radii seem to cause some problems like that.
The code can be viewed at:
http://bl.ocks.org/andrew-reid/3375e602cc6c00c4e3ea4799d171ee27
Looking at it, I feel like I want to add the option to add the inverse of the chevron to the rear end of the arcs for a better visual effect, but that's a different problem.

I would just use d3's path generator with an SVG marker. Add any shape to any path. Edit the "ends" by editing the marker definition. Use D3 path generator to define your arc (or any path).
It's worth noting that if you take this approach you have to use the d3 path generator rather than the d3 arc generator because the arc generator implicitly closes the path (putting your "end" marker back at the beginning of the path).
In my example, I added the chevron to the start as well just to show that it's as trivial as adding .attr("marker-start","url(#chevron)") and .attr("marker-end","url(#chevron)")
D3 Path Generator | https://github.com/d3/d3-path/blob/master/README.md#path
SVG Markers | https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker
edit: and now that I think of it, you can probably use d3.symbols to generate your markers/ends for you instead of manually defining the shape path. The chevron would have to be custom, but you could probably use the triangle symbol.
D3 Symbols | https://github.com/d3/d3-shape#symbols
console.clear()
var path = d3.path()
path.arc(225,80,70,1,-.5)
var path2 = d3.path()
path2.moveTo(20,20)
path2.bezierCurveTo(150,300,200,0,450,100)
d3.select("svg").append("path")
.attr("d", path2.toString())
.attr("stroke","steelblue")
.attr("fill","none")
.attr("stroke-width","20")
.attr("marker-start","url(#chevron)")
.attr("marker-end","url(#chevron)")
d3.select("svg").append("path")
.attr("d", path.toString())
.attr("stroke","#43A2CA")
.attr("fill","none")
.attr("stroke-width","20")
.attr("marker-start","url(#chevron)")
.attr("marker-end","url(#chevron)")
<script src="https://unpkg.com/d3#4.4.0"></script>
<?xml version="1.0"?>
<svg width="500" height="200" viewBox="0 0 500 200">
<defs>
<marker id="chevron"
viewBox="0 0 20 20" refX="10" refY="10"
markerUnits="userSpaceOnUse"
markerWidth="20" markerHeight="20"
orient="auto" fill="black">
<path d="M0 0 10 0 20 10 10 20 0 20 10 10Z" />
</marker>
</defs>
</svg>

Related

Document flowchart in Raphael

I know how to draw a simple rectangle in Raphael and I understand the sense of all its parameters. For example, these parameters give a nice rectangle with width equal to 201 and height equal to 179
M0,0 L0,179 L210,179 L210,0 L0,0Z
But I do not want a simple rectangle, I want a document flowchart which should look like this
I know from here, that in Raphael I can draw curved lines, for example with these parameters:
M150,150 A100,70 0 0,0 250,220
But unfortunatelly, the book does not explain the sense of these parameters. I know what M means, but I do not know what A means and all the following coordinates.
So, how can I fix my initial rectangle coordinates to get a document flowchart?
Your missing piece here is the SVG Path Spec.
Your initial rectangle:
M0,0 L0,179 L210,179 L210,0 L0,0Z
...is read as "go to 0,0, then draw a line to 0,179, then draw a line to 210,179, then draw a line to 210,0, then draw a line to 0,0 and return to the start." (The last part, the Z, is a little superfluous, since we've already closed the path.)
You want to replace the second line – from 0,179 to 210,179 – with an arc. I'm not a designer but I'd spitball that maybe a Quadratic Bezier Curve would do the trick:
M0,0 L0,179 Q53,159 105,179 T210,179 L210,0 L0,0Z
That means, starting at the Q, "draw a quadratic Bezier curve from the start point [0,179] to 105,179 using 53,159 as the control point. Then draw another from 105,179 to 210,179 using a reflection of the last control point." I haven't tested this path, so you may need to tweak the control point to get the curve you want. (Increasing the y distance between the control point and 179 will make a more dramatic curve; decreasing it will make a more gentle curve.)
The Raphael documentation explains more about using paths in Raphael.

d3: Contour or Surface plot from Irregular/Scattered Data

I can take a set of triplets [X,Y,Z] and immediately generate a (smooth) contour plot using Python and matplotlib with a single call to tricontour(). One can also generate contours 'easily' using plot.ly, but I find it to be unacceptably slow. (Also, I'm not interested in the MATLAB solution, which is similar to the Python)
I'm looking for similar functionality using d3.js. I would settle for a "surface plot" instead of contours, or a "heat map" without contour lines.
I can see how to generate a colored Delaunay triangulation and/or a colored Voronoi Tesselation, but the question of how to generate a contour plot in d3 from irregular data points seems to still be an open one (even though the question on this was prematurely closed!).
So far, all I've seen are approaches "by hand", using Radial basis functions (gaussian blur) or grid interpolation using Barycentric interpolation.
I'd even be willing to 'live with' Gouraud-shading or Coon-gradients on a Delaunay triangulation, but apparently "advanced shading methods" like Gourand or Coon gradients are not in "regular" SVG but are proposed for SVG2...not sure where that leaves me with d3 & (regular) SVG. It seems like doing this SVG gradient-shading by hand would be a major pain.
Is there a "better" package-y way to do this, i.e. something that doesn't require so much 'custom' code? (Maybe via some multidimensional Bezier routine I haven't found yet?)
I'll post a Fiddle with my starting point: a colored Voronoi tesselation: https://jsfiddle.net/k2v2jy7s/1/. Can you help me take this from "blocky" to "smooth" (and maybe even show contour lines)?
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var npoints = 1000;
var sites = d3.range(npoints)
.map(function(d) { return [Math.random() * width, Math.random() * height]; });
// values at data points / colors being mapped = "zvals"
var kx = 3.14159/(width*0.5);
var ky = 3.14159/(height*0.5);
var zvals = d3.range(npoints)
for (i = 0; i < npoints; i++) {
zvals[i] = (1.0 + Math.cos(kx*sites[i][0]) * Math.cos(ky*sites[i][1]))/2.0;
zvals[i] *= zvals[i];
}
var g = svg.append("g")
.attr("transform", "translate(" + 0+ "," + 0 + ")");
var voronoi = d3.voronoi()
.extent([[-1, -1], [width + 1, height + 1]]);
var polygon = svg.append("g")
.attr("class", "polygons")
.selectAll("path")
.data(voronoi.polygons(sites))
.enter().append("path")
.style('fill', function(d,i){ return d3.hsl( zvals[i]*310, 1, .5); })
.call(redrawPolygon);
function redrawPolygon(polygon) {
polygon
.attr("d", function(d) { return d ? "M" + d.join("L") + "Z" : null; });
}
</script>
Update: Also found this blocks.org post on "Gradient Heatmaps", which as I mentioned is the sort of result I'd be willing to live with, but again that's a large quantity of custom code. Would really prefer a compact 'stock' solution, a la tricontour().
5 ½ years, and no answers to this question!
Well, I've also been looking into how to generate contours from a series of [X,Y,Z] points in Javascript, but have not yet found the best or most complete solution. A lot of solutions I came across via Googling (such as d3-contour) are designed for an evenly spaced grid of values, not an irregular series of points as you might obtain from a land survey.
d3-tricontour
The d3-tricontour library looks perhaps the most promising, though, so I might have a play around with it.
Here's an example of what it can generate:
(The labels are optional.)
Apparently it uses the delaunay and meandering triangles algorithms to convert arbitrary points into triangles and then contour geometry. The algorithm works in O(n) where n is the number of edges, meaning it's very fast and scales perfectly well.
To learn more you can visit their:
Github repository
Many examples on Observable
Alternatives
Otherwise, there might be other ways to do this. If working with one of the grid-based libraries, I think the general process would be to:
Convert arbitary [X,Y,Z] points into a grid — the Delaunay algorithm is probably a great place to start (see d3-delaunay or other delaunay libraries)
Find the Z value for each point in the grid using some kind of interpolation (the maths for that, I'm not sure about)
Then feed that result into one of the grid-based contouring libraries
Constraining Contours
Also take note that creating contours from real world terrain also requires "constraining" some edges so that contours don't crossover ridgelines where they shouldn't.
CDT-JS is a library web app (with no separate library available as yet) that calculates constrained Delaunay triangulation, which might be useful for this case.
Otherwise, in theory, you might be able to create this kind of functionality by injecting additional [X,Y,Z] points along your lines of contraint prior to rendering. But I haven't tested this approach.

D3 geo: getting projection.clipAngle to work on all specified elements

I'm a newcomer to D3 and I'm trying to make a world globe with some points ("pins") on it. Demo here: http://bl.ocks.org/nltesown/66eee134d6fd3babb716
Quite commonly, the projection is defined as:
var proj = d3.geo.orthographic()
.center([0, 0])
.rotate([50, -20, 0])
.scale(250)
.clipAngle(90)
.translate([(width / 2), (height / 2)]);
the clipAngle works well for the svg paths, but not the pins (which are svg circles). As you can see on the demo, the pin that sits between Iceland and Greenland should be hidden (it's Taiwan).
So I suppose the problem comes from these lines, but I can't understand why:
.attr("transform", function(d) {
return "translate(" + proj([ d.lng, d.lat ]) + ")";
});
It is not sufficient to just set the clipping radius via clipAngle() to get the desired behavior. The projection alone will not do the clipping, but just calculate the projected coordinates without taking into account any clipping. That is the reason, why Taiwan gets rendered, although you expected it to be hidden.
But, thanks to D3, salvation is near. You just need to re-think the way you are inserting your circles representing places. D3 has the mighty concept of geo path generators which will take care of the majority of the work needed. When fed a projection having a clipping angle set, the path generator will take this into account when calculating which features to actually render. In fact, you have already set up a proper path generator as your variable path. You are even correctly applying it for the globe, the land and the arcs.
The path generator will operate on GeoJSON data, so all you need to do is convert your places to valid GeoJSON features of type Point. This could be done with a little helper function similar to that used for the arcs:
function geoPlaces(places) {
return places.map(function(d) {
return {
type: "Point",
coordinates: [d.lng, d.lat]
};
});
}
With only minor changes you are then able to bind these GeoJSON data objects to make them available for the path generator which in turn takes care of the clipping:
svg.selectAll(".pin") // Places
.data(geoPlaces(places))
.enter().append("path")
.attr("class", "pin")
.attr("d", path);
Have a look at my fork of your example for a working demo.

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)

D3 curved labels in the center of arc

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.

Categories