Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Polylines in D3 and Legend spacing

I am using the below example and wanted to have the legend outside the Pie chart and also have the Polyline for the Text and the count and Percentage for each slice.

With the current code I have Pie inside the pie and Text and Percentage are showing when I mouse over the slice.

Appreciate the help a lot.Thanks

Can some one please help as I am unable to move forward.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
    <link rel="stylesheet" href="normalize.css">
    <style>
        #chart {
            height: 360px;
            margin: 0 auto; /* NEW */
            position: relative;
            width: 360px;
        }

        .tooltip {
            background: #eee;
            box-shadow: 0 0 5px #999999;
            color: #333;
            display: none;
            font-size: 12px;
            left: 130px;
            padding: 10px;
            position: absolute;
            text-align: center;
            top: 95px;
            width: 80px;
            z-index: 10;
        }

        .legend {
            font-size: 12px;
        }

        rect {
            cursor: pointer; /* NEW */
            stroke-width: 2;
        }

            rect.disabled { /* NEW */
                fill: transparent !important; /* NEW */
            }
        /* NEW */
        h1 { /* NEW */
            font-size: 14px; /* NEW */
            text-align: center; /* NEW */
        }
        /* NEW */
    </style>
</head>
<body>

    <div id="chart"></div>
    <script src="Scripts/d3.v3.min.js"></script>
    <script>
      (function(d3) {
        'use strict';

        var width = 360;
        var height = 360;
        var radius = Math.min(width, height) / 2;
        var donutWidth = 75;
        var legendRectSize = 18;
        var legendSpacing = 4;

        var color = d3.scale.category20(); //builtin range of colors

        var svg = d3.select('#chart')
          .append('svg')
          .attr('width', width)
          .attr('height', height)
          .append('g')
          .attr('transform', 'translate(' + (width / 2) +
            ',' + (height / 2) + ')');

        var arc = d3.svg.arc()
          .innerRadius(radius - donutWidth)
          .outerRadius(radius);

        var pie = d3.layout.pie()
          .value(function(d) { return d.count; })
          .sort(null);

        var tooltip = d3.select('#chart')
          .append('div')
          .attr('class', 'tooltip');

        tooltip.append('div')
          .attr('class', 'label');

        tooltip.append('div')
          .attr('class', 'count');

        tooltip.append('div')
          .attr('class', 'percent');

        d3.csv('weekdays.csv', function(error, dataset) {
          dataset.forEach(function(d) {
            d.count = +d.count;
            d.enabled = true;                                         // NEW
          });

          var path = svg.selectAll('path')
            .data(pie(dataset))
            .enter()
            .append('path')
            .attr('d', arc)
            .attr('fill', function(d, i) {
              return color(d.data.label);
            })                                                        // UPDATED (removed semicolon)
            .each(function(d) { this._current = d; });                // NEW

          path.on('mouseover', function(d) {
            var total = d3.sum(dataset.map(function(d) {
              return (d.enabled) ? d.count : 0;                       // UPDATED
            }));
            var percent = Math.round(1000 * d.data.count / total) / 10;
            tooltip.select('.label').html(d.data.label);
            tooltip.select('.count').html(d.data.count);
            tooltip.select('.percent').html(percent + '%');
            tooltip.style('display', 'block');
          });

          path.on('mouseout', function() {
            tooltip.style('display', 'none');
          });

          /* OPTIONAL
          path.on('mousemove', function(d) {
            tooltip.style('top', (d3.event.pageY + 10) + 'px')
              .style('left', (d3.event.pageX + 10) + 'px');
          });
          */

          var legend = svg.selectAll('.legend')
            .data(color.domain())
            .enter()
            .append('g')
            .attr('class', 'legend')
            .attr('transform', function(d, i) {
              var height = legendRectSize + legendSpacing;
              var offset =  height * color.domain().length / 2;
              var horz = -2 * legendRectSize;
              var vert = i * height - offset;
              return 'translate(' + horz + ',' + vert + ')';
            });

          legend.append('rect')
            .attr('width', legendRectSize)
            .attr('height', legendRectSize)
            .style('fill', color)
            .style('stroke', color)                                   // UPDATED (removed semicolon)
            .on('click', function(label) {                            // NEW
              var rect = d3.select(this);                             // NEW
              var enabled = true;                                     // NEW
              var totalEnabled = d3.sum(dataset.map(function(d) {     // NEW
                return (d.enabled) ? 1 : 0;                           // NEW
              }));                                                    // NEW

              if (rect.attr('class') === 'disabled') {                // NEW
                rect.attr('class', '');                               // NEW
              } else {                                                // NEW
                if (totalEnabled < 2) return;                         // NEW
                rect.attr('class', 'disabled');                       // NEW
                enabled = false;                                      // NEW
              }                                                       // NEW

              pie.value(function(d) {                                 // NEW
                if (d.label === label) d.enabled = enabled;           // NEW
                return (d.enabled) ? d.count : 0;                     // NEW
              });                                                     // NEW

              path = path.data(pie(dataset));                         // NEW

              path.transition()                                       // NEW
                .duration(750)                                        // NEW
                .attrTween('d', function(d) {                         // NEW
                  var interpolate = d3.interpolate(this._current, d); // NEW
                  this._current = interpolate(0);                     // NEW
                  return function(t) {                                // NEW
                    return arc(interpolate(t));                       // NEW
                  };                                                  // NEW
                });                                                   // NEW
            });                                                       // NEW

          legend.append('text')
            .attr('x', legendRectSize + legendSpacing)
            .attr('y', legendRectSize - legendSpacing)
            .text(function(d) { return d; });

        });

      })(window.d3);
    </script>
</body>
</html>
like image 815
user1046415 Avatar asked Feb 06 '23 20:02

user1046415


2 Answers

You can place the legends where ever you wish by making the legends in a group and placing it using the translate

First Make SVG:

  var s = d3.select('#chart')
    .append('svg')
    .attr('width', width)
    .attr('height', height);

Now make a legend group:

  var legend_group = s.append('g').attr('transform',
    'translate(' + (width / 3) + ',' + (height / 1.4) + ')');

Use translate it to a place of your choice. I have moved it to (width/3, height/1.4)

Make a group in which the pie chart will be drawn.

  var svg = s.append('g')
    .attr('transform', 'translate(' + (width / 2) +
      ',' + (radius) + ')');

Lets make a polyline for each slice: This function will make as many polylines as the dataset length.

function makePolyLines() {
  var polyline = svg.selectAll("polyline")
    .data(pie(dataset), key);

  polyline.enter()
    .append("polyline");
  //hide polyline for which value is 0, a case when legend is clicked.
  svg.selectAll("polyline").style("display", function(d) {
    if (d.value == 0) {
      return "none";
    } else {
      return "block";
    }
  });

  polyline.transition().duration(1000)
    .attrTween("points", function(d) {
      this._current = this._current || d;
      var interpolate = d3.interpolate(this._current, d);
      this._current = interpolate(0);
      return function(t) {
        var d2 = interpolate(t);
        var pos = outerArc.centroid(d2);
        pos[0] = radius * 0.95 * (midAngle(d2) < Math.PI ? 1 : -1);
        return [arc.centroid(d2), outerArc.centroid(d2), pos];
      };
    });

  polyline.exit()
    .remove();
}

Similarly make text for labels.

function makeTexts() {
  var text = svg.selectAll(".labels")
    .data(pie(dataset), key);

  text.enter()
    .append("text")
    .attr("dy", ".35em")
    .classed("labels", true)
    .text(function(d) {
      return d.data.label + " (" + d.data.count + ")";
    });
  //hide text for which value is 0, a case when legend is clicked.
  svg.selectAll(".labels").style("display", function(d) {
    if (d.value == 0) {
      return "none";
    } else {
      return "block";
    }
  });

  text.transition().duration(1000)
    .attrTween("transform", function(d) {

      this._current = this._current || d;
      var interpolate = d3.interpolate(this._current, d);
      this._current = interpolate(0);
      return function(t) {
        var d2 = interpolate(t);
        var pos = outerArc.centroid(d2);
        pos[0] = radius * (midAngle(d2) < Math.PI ? 1 : -1);
        return "translate(" + pos + ")";
      };
    })
    .styleTween("text-anchor", function(d) {
      this._current = this._current || d;
      var interpolate = d3.interpolate(this._current, d);
      this._current = interpolate(0);
      return function(t) {
        var d2 = interpolate(t);
        return midAngle(d2) < Math.PI ? "start" : "end";
      };
    });

  text.exit()
    .remove();
}

finally call these two functions.

1) initially after the data is fetched.

2) whenever legend is clicked and the piechart is updated.

Working code here

like image 170
Cyril Cherian Avatar answered Feb 10 '23 09:02

Cyril Cherian


First, you need to make the svg element wider. Currently it's var width = 360;, you can change it to var width = 700; for example.

After you gained some more space, determine the width of the legend, for the example let's use 300px. Declare a new variable: var legendWidth = 300;

Now, when the legend is being declared:

var legend = svg.selectAll('.legend')
    .data(color.domain())
    .enter()
    .append('g')
    .attr('class', 'legend')
    .attr('transform', function(d, i) {
      var height = legendRectSize + legendSpacing;
      var offset =  height * color.domain().length / 2;
      var horz = (-2 * legendRectSize);
      var vert = i * height - offset;
      return 'translate(' + (horz) + ',' + vert + ')';
    });

When calculation to horizontal translation, we need to take the legendWidth into consideration:

var horz = (-2 * legendRectSize) - legendWidth;

Note: You will need to fix the left and top CSS properties for the .tooltip element.

Another note: If you want to take this solution to the next level, you can implement it in a dynamic way instead of having the "magic number" of var legendWidth = 300.

like image 45
Gilad Artzi Avatar answered Feb 10 '23 11:02

Gilad Artzi