Reputation: 1290
I want to use annotations on my horizontal d3 bar chart, which I successfully applied. Currently, an annotation is connected through a <line>
to the respective bar using the bar's end coordinates (a.k.a. because it is a horizontal bar chart its width
) and the annotation's getBBox()
coordinates. However, I would like the line to be curved rather than a straight line. I know that I need to use a <path>
for that, but how can I apply a curve to a path when only the starting and ending point are known?
FYI: I don't want to hard code the coordinates because the bar chart is animated and keep changes the bar's width
.
How can I make the path being curved with only knowing the starting and ending coordinates?
Upvotes: 1
Views: 1889
Reputation: 38211
One option I've used is a custom curve with d3 (d3-shape). This allows the code to work with both Canvas and SVG. It also allows linking together any number of points with the prescribed pattern.
The documentation for custom curves is a little useful, but it might be more useful to see an example:
var curve = function(context) {
var custom = d3.curveLinear(context);
custom._context = context;
custom.point = function(x,y) {
x = +x, y = +y;
switch (this._point) {
case 0: this._point = 1;
this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
this.x0 = x; this.y0 = y;
break;
case 1: this._point = 2;
default:
var x1 = this.x0 * 0.5 + x * 0.5;
var y1 = this.y0 * 0.5 + y * 0.5;
var m = 1/(y1 - y)/(x1 - x);
var r = -100; // offset of mid point.
var k = r / Math.sqrt(1 + (m*m) );
if (m == Infinity) {
y1 += r;
}
else {
y1 += k;
x1 += m*k;
}
this._context.quadraticCurveTo(x1,y1,x,y);
this.x0 = x; this.y0 = y;
break;
}
}
return custom;
}
Here for the first point we simply record the point, for subsequent points we draw a quadratic curve from the current point ([x,y]) to the previous point ([x0,y0]). In the example above, [x1,y1] is the control point - which is offset a perpendicular amount the line connecting [x,y] and [x0,y0]:
var curve = function(context) {
var custom = d3.curveLinear(context);
custom._context = context;
custom.point = function(x,y) {
x = +x, y = +y;
switch (this._point) {
case 0: this._point = 1;
this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
this.x0 = x; this.y0 = y;
break;
case 1: this._point = 2;
default:
var x1 = this.x0 * 0.5 + x * 0.5;
var y1 = this.y0 * 0.5 + y * 0.5;
var m = 1/(y1 - y)/(x1 - x);
var r = -50; // offset of mid point.
var k = r / Math.sqrt(1 + (m*m) );
if (m == Infinity || m == -Infinity) {
y1 += r;
}
else {
y1 += k;
x1 += m*k;
}
this._context.quadraticCurveTo(x1,y1,x,y);
this.x0 = x; this.y0 = y;
break;
}
}
return custom;
}
// Basic horizontal bar graph:
var svg = d3.select("body").append("svg")
.attr("width",500)
.attr("height", 400);
var data = [5,6,7];
var x = d3.scaleLinear()
.domain([0,10])
.range([0,320]);
var line = d3.line()
.curve(curve)
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; })
var g = svg.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("transform",function(d,i) {
return "translate("+[40,i*90+30]+")"
});
g.append("rect")
.attr("width",x)
.attr("height", 40)
g.append("path")
.attr("d", function(d,i) {
return line([[0,-3],[x(d),-3]])
})
path {
stroke-width: 1px;
stroke: black;
fill:none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I'd create a canvas example, but the principle is exactly the same from the line perspective, the only difference is we would use d3.line().context(context).curve(...
Upvotes: 2
Reputation: 13129
You can draw a <path>
with a d attribute, as is described in the SVG path spec (https://www.w3.org/TR/SVG/paths.html#DProperty).
To do so, you can calculate the midpoint of the two points by simply taking the average x and y values and draw a line using the Q
quadratic Bezier curve command. For example, if x1y1 is your start point and x2y2 is your end point, calculate the middle x3y3 and draw it like so: d='M x1,y1 Q x2,y2 x3,y3'
Upvotes: 0