block by enjalot 6457608

6457608

Full Screen

Sparkline Directive for Angular with d3.js

The reusable chart pattern makes it easy to make components responsive to changes in data as well as dimension.

Integrating with Angular based on code by @milr0c: Angular.js + d3.js

Miles’ talk on reusable charts with MV* Frameworks:Video

Shirley discusses a very similar pattern with Backbone: Video

index.html

<!DOCTYPE html>
  <head>
    <meta charset="utf-8">
    <link type="text/css" rel="stylesheet" href="style.css"/>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
    <script src="//d3js.org/d3.v3.min.js"></script>
    <script src="perlin.js"></script>
    <script src="sparkline.js"></script>
  </head>
  <body>
    <div ng-app="main">
      <div ng-controller="MainController">
        <div class="span2">
          <button class="btn btn-success" ng-click="update()">update</button>
        </div>
        <chart-sparkline data="data"></chart-sparkline>
      </div>
    </div>
    <script type="text/javascript">
      var main = angular.module('main', [])
      .config(['$routeProvider', function($routeProvider) {
        $routeProvider.when('/', { controller: MainController });
      }]);

      // Controllers
      var MainController = ['$scope', function($scope) {
        // create random data
        $scope.randomize = function(n, y) {
          if (arguments.length < 2) y = 1;
          if (!arguments.length) n = 30;
          return d3.range(n).map(function(d) { return Math.random() })
        };
        $scope.update = function() {
          //TODO: make this come from API
          $scope.updates++;
          $scope.data = $scope.randomize();
        };
        // Models
        //initial values
        $scope.updates = 0;
        $scope.data = $scope.randomize();
      }];

      // Views
      main.directive('chartSparkline', function() {
        var sparkline = charts.sparkline();
        return {
          restrict: 'E',
          replace: true,
          template: '<div class="chart"></div>',
          scope: {
            data: '=',
          },
          link: function($scope, $element, $attr) {
            //we select the element of this directive
            var div = d3.select($element[0]);
            //we calculate it's dimensions so we can be responsive
            var bbox = div.node().getBoundingClientRect();
            sparkline.width(bbox.width || 900);
            sparkline.height(bbox.height || 400 - 50);
            window.onresize = function() {
              bbox = div.node().getBoundingClientRect();
              sparkline.width(bbox.width || 900);
              sparkline.height(bbox.height || 400 - 50);
              //this is how you update the chart
              div.call(sparkline);
            }
            //we update the chart when the data get's updated
            $scope.$watch('data', function(newVal, oldVal) {
              if(newVal) div.datum(newVal).call(sparkline);
            });
          }
        }
      });
    </script>
  </body>
</html>

sparkline.js

charts = {};

charts.sparkline = function() {
  // basic data
  var margin = {top: 0, bottom: 50, left: 0, right: 0},
      width = 900,
      height = 400,
      //accessors
      xValue = function(d, i) { return i; },
      yValue = function(d) { return d; },
      // chart underpinnings
      x = d3.scale.ordinal(),
      y = d3.scale.linear(),
      // chart enhancements
      elastic = {
        x: true,
        y: true
      },
      convertData = true,
      duration = 500,
      formatNumber = d3.format(',d');

  function render(selection) {
    selection.each(function(data) {
      // setup the basics
      var w = width - margin.left - margin.right,
          h = height - margin.top - margin.bottom;

      if (convertData) {
        data = data.map(function(d, i) {
          return {
            x: xValue.call(data, d, i),
            y: yValue.call(data, d, i)
          };
        });
      }
      // set scales
      if (elastic.x) x.domain(data.map(function(d) { return d.x; }));
      if (elastic.y) y.domain([d3.min(data, function(d) { return d.y}), d3.max(data, function(d) { return d.y; })]);
      console.log("data", data)
      console.log("bounds", x.domain(), y.domain())
      x.rangeRoundBands([0, w], .1);
      y.range([h, margin.bottom]);

      var line = d3.svg.line()
        .x(function(d) { return x(d.x) })
        .y(function(d) { return y(d.y) })
        .interpolate("basis")

      var svg = selection.selectAll('svg').data([data]);
      var chartEnter = svg.enter()
        .append('svg')
          .attr('width', w)
          .attr('height', h)
        .append('g')
          .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
          .classed('chart', true);
      var chart = svg.select('.chart');


      path = chart.selectAll('.path').data([data]);

      path.enter()
        .append('path')
          .classed('path', true)
          .attr('d', line)

      path.transition()
        .duration(duration)
          .attr('d', line)

      path.exit()
        .transition()
        .duration(duration)
        .style('opacity', 0)
        .remove();
    });
  }

  // basic data
  render.margin = function(_) {
    if (!arguments.length) return margin;
    margin = _;
    return render;
  };
  render.width = function(_) {
    if (!arguments.length) return width;
    width = _;
    return render;
  };
  render.height = function(_) {
    if (!arguments.length) return height;
    height = _;
    return render;
  };

  // accessors
  render.xValue = function(_) {
    if (!arguments.length) return xValue;
    xValue = _;
    return render;
  };
  render.yValue = function(_) {
    if (!arguments.length) return yValue;
    yValue = _;
    return render;
  };

  render.x = function(_) {
    if (!arguments.length) return x;
    x = _;
    return render;
  };
  render.y = function(_) {
    if (!arguments.length) return y;
    y = _;
    return render;
  };

  // chart enhancements
  render.elastic = function(_) {
    if (!arguments.length) return elastic;
    elastic = _;
    return render;
  };
  render.convertData = function(_) {
    if (!arguments.length) return convertData;
    convertData = _;
    return render;
  };
  render.duration = function(_) {
    if (!arguments.length) return duration;
    duration = _;
    return render;
  };
  render.formatNumber = function(_) {
    if (!arguments.length) return formatNumber;
    formatNumber = _;
    return render;
  };

  return render;
};

style.css

body {
  font: 14px helvetica;
  width: 100%;
  height: 100%;
}

.chart {
  width: 100%;
  height: 100%;
}

.path {
  fill: none;
  stroke: #000;
  stroke-width: 3px;
}

.selected {
  stroke: #78C656;
}
.btn {
  font-size: 40px;
  background-color: #efefef;
}