3

I am trying to create a Bar chart using D3.js. The condition is that Bar should have fixed width and padding between the bars and it should be plot at center of grid lines.

Tooltip should appear on click with a vertical line

I am able to create the grid lines with plotted bar, somehow rx,ry is rounding from both sides. How can i acheive the same sort of result.

var rectCluster = svg.selectAll(".bar")
                    .data(data);

                rectCluster
                    .enter().append("rect")
                    .attr("class", function(d) {
                        return "bar";
                    })
                    .attr(attrs)
                    .attr({ry : (20), rx : 20 })
                    .attr("x", function(d) { return x(d.text); })
                    .attr("y", function(d) {
                        return height;
                    })
                    .style("fill", function(d) {
                        return color(d.text);
                    })
                    .attr("width", x.rangeBand())
                    .attr("height", 0)
                    .transition()
                    .duration(animationDelay)
                    .delay(function(d, i) {
                        return i * animationDelay;
                    })
                    .attr("y", function(d) { return y(d.score); })
                    .attr("height", function(d) { return height - y(d.score) });

                    var attrs = {
    width: function(d, i){
      return o.rangeBand();
    },
    height: function(d, i){
      return yScale(d);
    },
    fill: "#384252",
    x: function(d, i){
      return xScale(i);
    },
    y: function(d, i){
      return height - yScale(d) - margin.bottom;
    }
  };

Js Fiddle for the same

I am trying to achieve like this.enter image description here

itaydafna
  • 1,968
  • 1
  • 13
  • 26
Kunal Vashist
  • 2,380
  • 6
  • 30
  • 59
  • 1
    Use a clipping path: https://stackoverflow.com/questions/34923888/rounded-corner-only-on-one-side-of-svg-rect – nixkuroi Sep 17 '18 at 17:53
  • 1
    like nixkuroi said, a clip path and create a larger `rect` blow the x-axis ad 2*radius to your height (1 should also be enough) – rioV8 Sep 17 '18 at 18:20

2 Answers2

11

One option is to use a clip path, but you can also create a simple path generator using the same information you use to build the rectangles: x,y,width,height, plus radius. The path is fairly simple:

  1. Move to bottom left corner
  2. Line to bottom of top left arc
  3. Arc to top of top left arc
  4. Line to the top of the top right arc
  5. Arc to the bottom of the top right arc.
  6. Line to the bottom right corner
  7. Close path.

Which is comes together something like:

  1. M x,y
  2. L x,y-height+radius
  3. A radius,radius,0,0,1,x+radius,y-height
  4. L x+width-r,y-height
  5. A radius,radius,0,0,1,x+width,y-height+radius
  6. L x+width,y
  7. Z

Which could look like this (a rather lazy implementation):

function bar(x,y,w,h,r,f) {
    // Flag for sweep:
    if(f == undefined) f = 1;
    // x coordinates of top of arcs
    var x0 = x+r;
    var x1 = x+w-r;
    // y coordinates of bottom of arcs
    var y0 = y-h+r;

    // assemble path:
    var parts = [
      "M",x,y,               // step 1
      "L",x,y0,              // step 2
      "A",r,r,0,0,f,x0,y-h,  // step 3
      "L",x1,y-h,            // step 4
      "A",r,r,0,0,f,x+w,y0,  // step 5
      "L",x+w,y,             // step 6
      "Z"                    // step 7
     ];
    return parts.join(" ");
}

I've included an optional sweep flag (f) - it'll invert the arcs if set to 0.

And applied something like so:

 .attr("d", function(d) { 
    return bar(x(d),y(0),x.bandwidth(),y(0)-y(d),15);  
  })

Put together you might get something like:

var width = 500;
var height = 200;
var svg = d3.select("body").append("svg").attr("width",width).attr("height",height);
var data = [ 10,20,30,40,20,50,60 ];

var x = d3.scaleBand().domain(d3.range(data.length)).range([10,width-10]).paddingInner(0.1);
var y = d3.scaleLinear().domain([0,60]).range([height-10,10]);

var bars = svg.selectAll(null)
  .data(data)
  .enter()
  .append("path")
  .attr("d", function(d,i) { 
   return bar(x(i),y(0),x.bandwidth(),y(0)-y(d),10);  
  })
  
function bar(x,y,w,h,r,f) {
 // Flag for sweep:
 if(f == undefined) f = 1;
 
 // x coordinates of top of arcs
 var x0 = x+r;
 var x1 = x+w-r;
 // y coordinates of bottom of arcs
 var y0 = y-h+r;
 // just for convenience (slightly different than above):
 var l = "L", a = "A";

 var parts = ["M",x,y,l,x,y0,a,r,r,0,0,f,x0,y-h,l,x1,y-h,a,r,r,0,0,f,x+w,y0,l,x+w,y,"Z"];
 return parts.join(" ");
}

// Still transitionable:
bars.data(data.reverse())
 .transition()
 .attr("d", function(d,i) { 
  return bar(x(i),y(0),x.bandwidth(),y(0)-y(d),30);  
 })
 .duration(2000)
 .transition()
 .attr("d", function(d,i) { 
  return bar(x(i),y(0),x.bandwidth(),y(0)-y(d),0);  
 })
 .duration(2000)
 .transition()
 .attr("d", function(d,i) { 
  return bar(x(i),y(0),x.bandwidth(),y(0)-y(d),15);  
  })
 .duration(2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

If you radius exceeds half bar height or width you'll get some funkier results, a complete implementation of this would consider a check to make sure that the radius for bar isn't too large

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • I cannot thank this guy enough, for providing good sample code! This is just one of those things where you do not want to spend time inventing the wheel. – Karl Johan Vallner Jan 15 '19 at 16:34
  • This solution has some small issues when the when there is a big gap between the max number and the min number. https://jsfiddle.net/ramseyfeng/3yaz79cn/2/ I have update the max y value from 60 to 10000, and there is some redundant path at the bottom, do you know how to solve the issue? Thanks! – huan feng Jan 15 '21 at 02:53
  • 1
    @huanfeng. if the radius exceeds the plotted height of the bar, it will go below the axis, which is what you see. You could pass the minimum of the radius and bar height as the radius parameter using `Math.min(10,y(0)-y(d)));` so that the radius cannot exceed the bar height [eg](https://jsfiddle.net/72h8tync/). This would avoid that issue, or alternatively, calculate the minimum within the bar function itself. – Andrew Reid Jan 15 '21 at 03:59
0

My first option would be to use clipPath (answer here: Rounded corner only on one side of svg <rect>)

Another option is to simply draw another rectangle at the bottom that has square corners:

rectCluster
                    .enter().append("rect")
                    .attr("class", function(d) {
                        return "bar";
                    })
                    .attr(attrs)
                    .attr({ry : (20), rx : 20 })
                    .attr("x", function(d) { return x(d.text); })
                    .attr("y", function(d) {
                        return height;
                    })
                    .style("fill", function(d) {
                        return color(d.text);
                    })
                    .attr("width", x.rangeBand())
                    .attr("height", 0)
                    .transition()
                    .duration(animationDelay)
                    .delay(function(d, i) {
                        return i * animationDelay;
                    })
                    .attr("y", function(d) { return y(d.score); })
                    .attr("height", function(d) { return height - y(d.score) });

                 // SQUARE CORNERS
                 rectCluster
                    .enter().append("rect")
                    .attr("class", function(d) {
                        return "bar";
                    })
                    .attr(attrs)
                    .attr({ry : (20), rx : 0 })
                    .attr("x", function(d) { return x(d.text); })
                    .attr("y", 0)
                    .style("fill", function(d) {
                        return color(d.text);
                    })
                    .attr("width", x.rangeBand())
                    .attr("height", 0)
                    .transition()
                    .duration(animationDelay)
                    .delay(function(d, i) {
                        return i * animationDelay;
                    })
                    .attr("y", height-20)
                    .attr("height", 20);

See Fiddle:

https://jsfiddle.net/6x2y35gn/40/

nixkuroi
  • 2,259
  • 1
  • 19
  • 25