Search code examples
javascriptchartssnap.svg

Dynamically update Snap.svg gauge chart


I found some code for a chart on Codepen (credits: Henry Poydar) that uses snap.svg to create an animated gauge chart.

angular.module('app', []);

angular.module('app')
  .controller('metricsCtrl', function($scope) {


    $scope.percentage = .8;


    var polar_to_cartesian, svg_circle_arc_path, animate_arc;

    polar_to_cartesian = function(cx, cy, radius, angle) {
      var radians;
      radians = (angle - 90) * Math.PI / 180.0;
      return [Math.round((cx + (radius * Math.cos(radians))) * 100) / 100, Math.round((cy + (radius * Math.sin(radians))) * 100) / 100];
    };

    svg_circle_arc_path = function(x, y, radius, start_angle, end_angle) {
      var end_xy, start_xy;
      start_xy = polar_to_cartesian(x, y, radius, end_angle);
      end_xy = polar_to_cartesian(x, y, radius, start_angle);

      return "M " + start_xy[0] + " " + start_xy[1] + " A " + radius + " " + radius + " 0 0 0 " + end_xy[0] + " " + end_xy[1];
    };

    animate_arc = function(ratio, svg, perc) {
      var arc;
      arc = svg.path('');

      return Snap.animate(0, ratio, (function(val) {
        var path;
        arc.remove();

        path = svg_circle_arc_path(500, 500, 450, -90, val * 180.0 - 90);
        arc = svg.path(path);
        arc.attr({
          class: 'data-arc'
        });
        perc.text(Math.round(val * 100) + '%');
      }), Math.round(2000 * ratio), mina.easeinout);
    };

    $scope.$watch('percentage', function() {

      $('.metric').each(function() {
        var ratio, svg, perc;
        //ratio = $(this).data('ratio');
        ratio = $scope.percentage;

        svg = Snap($(this).find('svg')[0]);
        perc = $(this).find('text.percentage');
        animate_arc(ratio, svg, perc);
      });
    });

  });
.metric {
  padding: 10%;
}
.metric svg {
  max-width: 100%;
}
.metric path {
  stroke-width: 75;
  stroke: #ecf0f1;
  fill: none;
}
.metric path.data-arc {
  stroke: #3498db;
}
.metric text {
  fill: #3498db;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js"></script>


<div ng-app="app" ng-controller="metricsCtrl">
  <div class="metric">
    <svg viewBox="0 0 1000 500">
      <path d="M 950 500 A 450 450 0 0 0 50 500"></path>
      <text class='percentage' text-anchor="middle" alignment-baseline="middle" x="500" y="300" font-size="140" font-weight="bold">0%
      </text>
      <text class='title' text-anchor="middle" alignment-baseline="middle" x="500" y="450" font-size="90" font-weight="normal">Empty
      </text>
    </svg>
  </div>

  <input ng-model="percentage">
</div>

I would like to be able to dynamically update the chart's data and have the SVG render accordingly. I am able to get the chart to show an increased value, but a decrease in value is not working.

Here is a demo that reproduces my problem: http://codepen.io/EvanWieland/pen/bpxqpV. In the demo, if you increase the value in the input below the chart and then decrease it, you will be able to observe my dilemma. Note that the demo uses Angularjs, this is not a requirement.


Solution

  • This was due to "svg.path(path)" that creates a new arc each time, thus decreasing value draws an arc hidden by the previous ones. The solution is to remove the previous arc at each repaint.

    angular.module('app', []);
    
    angular.module('app')
      .controller('metricsCtrl', function($scope) {
    
    
        $scope.percentage = .8;
    
    
        var polar_to_cartesian, svg_circle_arc_path, animate_arc;
    
        polar_to_cartesian = function(cx, cy, radius, angle) {
          var radians;
          radians = (angle - 90) * Math.PI / 180.0;
          return [Math.round((cx + (radius * Math.cos(radians))) * 100) / 100, Math.round((cy + (radius * Math.sin(radians))) * 100) / 100];
        };
    
        svg_circle_arc_path = function(x, y, radius, start_angle, end_angle) {
          var end_xy, start_xy;
          start_xy = polar_to_cartesian(x, y, radius, end_angle);
          end_xy = polar_to_cartesian(x, y, radius, start_angle);
    
          return "M " + start_xy[0] + " " + start_xy[1] + " A " + radius + " " + radius + " 0 0 0 " + end_xy[0] + " " + end_xy[1];
        };
    
        animate_arc = function(ratio, svg, perc) {
          var arc;
          arc = svg.path('');
    
          return Snap.animate(0, ratio, (function(val) {
            var path;
            arc.remove();
    
            path = svg_circle_arc_path(500, 500, 450, -90, val * 180.0 - 90);
            var previousArc = svg.select('.data-arc')
            if (previousArc){
                previousArc.remove(); // REMOVES PREVIOUS ARC
            }
            arc = svg.path(path);
            arc.attr({
              class: 'data-arc'
            });
            perc.text(Math.round(val * 100) + '%');
          }), Math.round(2000 * ratio), mina.easeinout);
        };
    
        $scope.$watch('percentage', function() {
    
          $('.metric').each(function() {
            var ratio, svg, perc;
            //ratio = $(this).data('ratio');
            ratio = $scope.percentage;
    
            svg = Snap($(this).find('svg')[0]);
            perc = $(this).find('text.percentage');
            animate_arc(ratio, svg, perc);
          });
        });
    
      });
    .metric {
      padding: 10%;
    }
    .metric svg {
      max-width: 100%;
    }
    .metric path {
      stroke-width: 75;
      stroke: #ecf0f1;
      fill: none;
    }
    .metric path.data-arc {
      stroke: #3498db;
    }
    .metric text {
      fill: #3498db;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js"></script>
    
    
    <div ng-app="app" ng-controller="metricsCtrl">
      <div class="metric">
        <svg viewBox="0 0 1000 500">
          <path d="M 950 500 A 450 450 0 0 0 50 500"></path>
          <text class='percentage' text-anchor="middle" alignment-baseline="middle" x="500" y="300" font-size="140" font-weight="bold">0%
          </text>
          <text class='title' text-anchor="middle" alignment-baseline="middle" x="500" y="450" font-size="90" font-weight="normal">Empty
          </text>
        </svg>
      </div>
    
      <input ng-model="percentage">
    </div>