3

Right now, I have my bar bases styled as shadows using ellipses.: http://jsfiddle.net/rdesai/MjFgK/55/

How do I change the style so that the bases look like in the chart below?

enter image description here

EDIT

This problem can be cut down to creating a shape like the one below:
enter image description here
I can use this shape to replace the ellipses.

How do I modify the code below for ellipses into a shape like above?

svg.selectAll(".ellipse")
.data(data)
.enter()
.append("ellipse")
.attr("cx", function(d) { return x(d.episode) + 17; })
.attr("cy", function(d) { return y(0); })
.attr("rx", 20)
.attr("ry", 5)
.style("fill", function(d) { return d.shadow_color; })
Rahul Desai
  • 15,242
  • 19
  • 83
  • 138

1 Answers1

9

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.

AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
  • Thank you for the detailed answer. There is one small thing remaining actually. If you see the bottom corners of the first and last bar, they are curved on one side. How do I achieve that? – Rahul Desai Feb 27 '14 at 08:22
  • You'll need to make the function dependent on `i` (the index of the bar), and if `i` is zero, change the start point to be inside the bar instead of outside the bar (and maybe change the radius of that first curve to use the same radius as the top of the bars). Likewise, if `i` is equal to the data length, then move the points of the last curve. Or as an alternative, you could add a clipPath to the entire plot that is a rectangle with curved corners... That might be easier! – AmeliaBR Feb 27 '14 at 08:27
  • I see. Any takes on the x-axis line with the circles? – Rahul Desai Feb 27 '14 at 08:40
  • For the axis: draw a default d3 axis, then reselect all the "g.tick" elements and add a circle within each. You can either select and remove the tick lines, or just set them to zero length. [More on customizing d3 axes here](http://stackoverflow.com/a/21583985/3128209). Quick example of circle ticks, adapted from the example in that link: http://fiddle.jshell.net/zUj3E/6/ – AmeliaBR Feb 27 '14 at 16:02
  • I think there are two problems with the fiddle that you shared in the comment above - It doesnt work in Firefox and I tried to convert the y-axis to x-axis (replaced all y's with x'es) but no luck. How do I fix this? – Rahul Desai Feb 28 '14 at 04:52
  • 1
    Updated with the Firefox fix (they have a bug on `.getComputedStyle()` if the SVG isn't explicitly declared a block or inline-block element), and a change to a horizontal axis (you need to set the `.orient()` property of the axis, x and y are just labels/classnamess). http://fiddle.jshell.net/zUj3E/7/ – AmeliaBR Feb 28 '14 at 17:27
  • Sorry, the fiddle isnt working in Firefox. Can we use some different function other than `.getComputedStyle()`? – Rahul Desai Mar 04 '14 at 04:27
  • It was working, I'd just forgotten to change my range, which was still based on the SVG height. And while Chrome's default height for an SVG fills the window, Firefox's default is 5em I think. Sorry, should have actually tested it out... http://fiddle.jshell.net/zUj3E/9/ – AmeliaBR Mar 04 '14 at 16:19
  • IE has some problems with it. :( – Rahul Desai Mar 05 '14 at 09:07
  • I tried in IE11, including IE9 and 10 compatibility mode, the last fiddle looked fine. But there is a reasons so few d3 examples use responsive design -- it is quite a pain to deal with the different browsers' ways of sizing SVG. – AmeliaBR Mar 05 '14 at 16:20
  • IE8 doesn't support SVG natively, so I generally don't worry about it's poor support of ECMA Javascript. *But*, if you're using Raphael or something else to adapt your SVG to IE8, then you'll need to search for polyfill/substitute code for determining the width. – AmeliaBR Mar 06 '14 at 16:01