You're going to need to use a custom path-data generating function. If you're not familiar with how the path data attribute works you may find this tutorial a useful starting point (continued here and here ).
Since it's not any more work, I'm going to suggest drawing the entire bar as the custom path, rather than having a separate base. If you really want separate elements, it should be fairly straightforward to adapt.
Your code for the bars was:
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.episode); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.donations); })
.attr("height", function(d) { return height - y(d.donations); })
.style("fill", function(d) { return d.bar_color; })
.attr("rx", 10)
.attr("ry", 10)
.on("mouseover", function(d) {
tooltip.transition().duration(200).style("opacity", .9);
tooltip.html("NGO: " + d.ngo)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tooltip.transition().duration(500).style("opacity", 0);
});
The tooltip mouseover functions aren't going to change, so I'll leave those out from the rest of the discussion. The fill style is also not going to change, and neither is the class. However, we're going to turn the <rect>
elements into <path>
elements, and we're going to replace all the other attributes (x/y/width/height/rx/ry) with a path data "d" attribute:
svg.selectAll(".bar")
.data(data)
.enter().append("path")
.attr("class", "bar")
.style("fill", function(d) { return d.bar_color; })
.attr("d", function(d) {
return /* a path data string that completely describes this shape */;
})
.on("mouseover",
/* etc */
Now, let's analyse that shape. Starting from the bottom left, you need the following directions to draw it:
- move to the start of the flared base, in the middle of the padding between the bars, on the axis line;
- curve to the left edge of the bar, equal distance up from the axis as the flare radius;
- straight line almost to the top of the bar, stopping short by the top curve radius, which is half the bar width;
- semi-circle (or two curves) up to the full bar height then back down to the other side;
- straight line back down almost to the axis line;
- create the other flared base, curving out to halfway through the padding;
- close the path to connect back to the begin.
For the move command and the line command, you only need to give the destination (x,y) point. For the close command, you don't need to give any points at all. For curves it gets a little more complicated. You could do this with arcs, (see my second tutorial for the syntax), but it's a little bit simpler and looks much the same if you use quadratic curves, with the control point being the corner position that you're curving around. That is, a quadratic curve from (0,0) to (1,1) with control point (0,1) is almost an arc centered on (1,0).
(0,0)
@ — * << control point (0,1)
\
| ...something like this, but prettier!
@
(1,1)
With that plan of attack, and determining the width of the flare from the paddingProportion given as a parameter to x.rangeRoundBands()
, you get:
.attr("d", function(d) {
var barWidth = x.rangeBand();
var paddingWidth = barWidth*(paddingProportion)/(1-paddingProportion);
var flareRadius = paddingWidth/2;
var topRadius = barWidth/2;
var xPos = x(d.episode);
var yPos = y(d.donations);
return [ "M", [ (xPos - flareRadius), height], //start at bottom left of base
"Q", [xPos, height], //control point for flare curve
[xPos, (height-flareRadius)], //end point of flare curve
"L", [xPos, (yPos + topRadius)], //line to start of top curve
"Q", [xPos, yPos], //control point for left top curve
[(xPos + topRadius), yPos], //end point for left top curve
"Q", [(xPos + barWidth), yPos], //control point for right top curve
[(xPos + barWidth), (yPos + topRadius)], //end point for right top
"L", [(xPos + barWidth), (height-flareRadius)], //line to start of right flare
"Q", [(xPos + barWidth), height], //control for right flare
[(xPos + barWidth + flareRadius), height], //end of right flare
"Z" //close the path
].join(" ");
})
http://jsfiddle.net/rdesai/MjFgK/62/
The final returned value is a string, I'm putting it together as an array of letters and two-element arrays for each (x,y) point, then joining them all together with spaces. It's slightly faster than doing it with +
, and keeps the data in an organized structure. The final returned string looks like
M 47.75,195 Q 52,195 52,190.75 L 52,26.056240786240778 Q 52,9.056240786240778 69,9.056240786240778 Q 86,9.056240786240778 86,26.056240786240778 L 86,190.75 Q 86,195 90.25,195 Z
Now, that's mostly the shape that you want, but it gets a little messy on your shortest bars, where the radius of the top curve is greater than the height of the bar. A couple quick max/min checks, to make sure the y values of points are never smaller than yPos
or larger than height
, cleans things up:
.attr("d", function(d) {
var barWidth = x.rangeBand();
var paddingWidth = barWidth*(paddingProportion)/(1-paddingProportion);
var flareRadius = paddingWidth/2;
var topRadius = barWidth/2;
var xPos = x(d.episode);
var yPos = y(d.donations);
return [ "M", [ (xPos - flareRadius), height], //start at bottom left of base
"Q", [xPos, height],
[xPos, Math.max((height-flareRadius), yPos)],
"L", [xPos, Math.min((yPos + topRadius), height)],
"Q", [xPos, yPos],
[(xPos + topRadius), yPos],
"Q", [(xPos + barWidth), yPos],
[(xPos + barWidth), Math.min((yPos + topRadius), height)],
"L", [(xPos + barWidth), Math.max((height-flareRadius), yPos)],
"Q", [(xPos + barWidth), height],
[(xPos + barWidth + flareRadius), height],
"Z"
].join(" ");
})
http://jsfiddle.net/MjFgK/63/
That's a lot to take in, so I suggest you take some time and play around with it, try varying the shapes, try using arcs instead of quadratic curves, try to recreate the shapes from the first and last bars on your sample image. Or go back to your original plan of just creating the base shapes as separate elements.