1

I am displaying bar chart. Each bar is combination of a rect and a circle element. Now, I want to group each rect, circle into a group element like this.

<g class="bar_group">
  <rect class="bar" ..></rect>
  <circle class="rounded_edge" ..></circle>
 </g>
<g class="bar_group">
  <rect class="bar" ..></rect>
  <circle class="rounded_edge" ..></circle>
 </g> 

How to group them like above ie one group for a rect and circle pair only? right now it is coming as all rects and circles in one group only. ie a group under which all rects and circles.

var data = [
        {
          type: "s1",
          value: 4,
        },
        {
          type: "s2",
          value: 2,
        },
        {
          type: "s3",
          value: 5,
        },
      ];
      var margin = { top: 50, right: 20, bottom: 0, left: 30 };
      var width = 300;
      var height = 220;
      var innerHeight = height - margin.top - 10;
      var barWidth = 20;
      var barMidpoint = barWidth / 2;

      var svg = d3
        .select("svg")
        .attr("class", "d3svg")
        .attr("width", width)
        .attr("height", height);

      var svgG = svg.append("g").attr("transform", "translate( -6, 0)");
      // X AXIS CODE
      var xScale = d3.scaleBand().range([margin.left, width]).padding(0.4);

      xScale.domain(
        data.map(function (d) {
          return d.type;
        })
      );
      var xAxis = d3.axisBottom(xScale);
      // call x axis
      svgG
        .append("g")
        .attr("transform", `translate(20,${innerHeight})`)
        .call(xAxis);

      // Y AXIS CODE
      var yScale = d3.scaleLinear().range([innerHeight, margin.top]);
      yScale.domain([
        0,
        d3.max(data, function (d) {
          return d.value;
        }),
      ]);

      var yAxis = d3.axisLeft(yScale).tickFormat(function (d) {
        return d;
      });

      svgG
        .append("g")
        .attr("class", "y-axis")
        .attr("transform", `translate(${margin.left + 25},0)`)
        .call(yAxis);

      // DISPLAY BARS
      svgG
        .selectAll(".bar")
        .data(data)
        .enter()
        .append("rect")
        .attr("class", "bar")
        .attr("x", function (d) {
          return xScale(d.type) + 28;
        })
        .attr("y", function (d) {
          return yScale(d.value);
        })
        .attr("width", barWidth)
        .attr("height", function (d) {
          return innerHeight - yScale(d.value);
        });
      svgG
        .selectAll(".circle")
        .data(data)
        .enter()
        .append("circle")
        .attr("class", "bar")
        .attr("cx", function (d) {
          return xScale(d.type) + barMidpoint + 28;
        })
        .attr("cy", function (d) {
          return yScale(d.value);
        })
        .attr("r", barMidpoint);
 .bar {
        fill: steelblue;
      }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="600" height="500"></svg>
ram
  • 237
  • 2
  • 14

1 Answers1

1

Sought Solution

You currently do two enter cycles, one for the rects and one for the circles. For each you append to a common parent (svgG).

Instead, in order to achieve your goal, "to group each rect, circle into a group element", we can do one enter cycle for a parent g:

  // Create groups:
  var groups = svgG
    .selectAll(".group")
    .data(data)
    .enter()
    .append("g")
    .attr("class", "group")
    .attr("transform", function (d) {
      // will ease placement of circles/rects:
      return "translate("+(xScale(d.type) + 28)+","+ yScale(d.value) +")";
    })

  // append child elements, one of each per parent g:
  groups.append("circle")
    .attr("cx", barMidpoint)
    .attr("r", barMidpoint);

  groups.append("rect")
    .attr("width", barWidth)
    .attr("height", function (d) {
      return innerHeight - yScale(d.value);
    });

We have one g per item in the data array. Also, the datum of the parent is carried forward to the children, so you can still use function(d) when setting properties.

We also simplify the styling of the data and the positioning by applying style and positional properties to the g.

var data = [
        {
          type: "s1",
          value: 4,
        },
        {
          type: "s2",
          value: 2,
        },
        {
          type: "s3",
          value: 5,
        },
      ];
      var margin = { top: 50, right: 20, bottom: 0, left: 30 };
      var width = 300;
      var height = 220;
      var innerHeight = height - margin.top - 10;
      var barWidth = 20;
      var barMidpoint = barWidth / 2;

      var svg = d3
        .select("svg")
        .attr("class", "d3svg")
        .attr("width", width)
        .attr("height", height);

      var svgG = svg.append("g").attr("transform", "translate( -6, 0)");
      // X AXIS CODE
      var xScale = d3.scaleBand().range([margin.left, width]).padding(0.4);

      xScale.domain(
        data.map(function (d) {
          return d.type;
        })
      );
      var xAxis = d3.axisBottom(xScale);
      // call x axis
      svgG
        .append("g")
        .attr("transform", `translate(20,${innerHeight})`)
        .call(xAxis);

      // Y AXIS CODE
      var yScale = d3.scaleLinear().range([innerHeight, margin.top]);
      yScale.domain([
        0,
        d3.max(data, function (d) {
          return d.value;
        }),
      ]);

      var yAxis = d3.axisLeft(yScale).tickFormat(function (d) {
        return d;
      });

      svgG
        .append("g")
        .attr("class", "y-axis")
        .attr("transform", `translate(${margin.left + 25},0)`)
        .call(yAxis);

       // Create groups:
      var groups = svgG
        .selectAll(".group")
        .data(data)
        .enter()
        .append("g")
        .attr("class", "group")
        .attr("transform", function (d) {
          // will ease placement of circles/rects:
          return "translate("+(xScale(d.type) + 28)+","+ yScale(d.value) +")";
        })

      groups.append("circle")
        .attr("cx", barMidpoint)
        .attr("r", barMidpoint);

      groups.append("rect")
        .attr("width", barWidth)
        .attr("height", function (d) {
          return innerHeight - yScale(d.value);
        });
.group {
        fill: steelblue;
      }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="600" height="500"></svg>

Better Solution

The better solution is to use one path to draw your rounded shape, such as done here. We build a path generator to draw the shape, and then use that to create the path:

 svgG.selectAll(".bar")
  .data(data)
  .enter()
  .append("path")
  .attr("d", function(d) {
        pathGeneratorFunction(params);
   });

This way we avoid having two shapes to worry about when we are only representing one data point.

If we wanted to be fancier, we could design a function to simply take the datum of the point and return a path string with the following pattern:

   .attr("d", generator);

But, I'm just demonstrating the simpler version here:

var data = [
        {
          type: "s1",
          value: 4,
        },
        {
          type: "s2",
          value: 2,
        },
        {
          type: "s3",
          value: 5,
        },
      ];
      var margin = { top: 50, right: 20, bottom: 0, left: 30 };
      var width = 300;
      var height = 220;
      var innerHeight = height - margin.top - 10;
      var barWidth = 20;
      var barMidpoint = barWidth / 2;

      var svg = d3
        .select("svg")
        .attr("class", "d3svg")
        .attr("width", width)
        .attr("height", height);

      var svgG = svg.append("g").attr("transform", "translate( -6, 0)");
      // X AXIS CODE
      var xScale = d3.scaleBand().range([margin.left, width]).padding(0.4);

      xScale.domain(
        data.map(function (d) {
          return d.type;
        })
      );
      var xAxis = d3.axisBottom(xScale);
      // call x axis
      svgG
        .append("g")
        .attr("transform", `translate(20,${innerHeight})`)
        .call(xAxis);

      // Y AXIS CODE
      var yScale = d3.scaleLinear().range([innerHeight, margin.top]);
      yScale.domain([
        0,
        d3.max(data, function (d) {
          return d.value;
        }),
      ]);

      var yAxis = d3.axisLeft(yScale).tickFormat(function (d) {
        return d;
      });

      svgG
        .append("g")
        .attr("class", "y-axis")
        .attr("transform", `translate(${margin.left + 25},0)`)
        .call(yAxis);

      // DISPLAY BARS
      svgG
        .selectAll(".bar")
        .data(data)
        .enter()
        .append("path")
        .attr("class", "bar")
        .attr("d", function(d) {
           return bar(xScale(d.type) + 28,                 
                      innerHeight,
                      barWidth,
                      yScale(d.value),
                      barWidth/2,
                      1)
         })
  
  
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(" ");
}
.bar {
        fill: steelblue;
      }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="600" height="500"></svg>
Andrew Reid
  • 37,021
  • 7
  • 64
  • 83