Intro to D3 with Angular

Created by Victor Mejia

about me

Software Dev @ LoopNet (CoStar Group)

I ♥ JavaScript


Tweeting @victorczm

Coding @victormejia

Blogging @ victormejia.github.io

what is D3?

Data

Driven

Documents

Created by Mike Bostock

efficient manipulation of documents based on data

not a just a charting library, it's so much more

let's focus on the word visualization

check out what

D3 can do

Awesome Visualizations

D3 Gallery

D3 out in the wild:

Emmet Sublime Package

California Drought

Simpson's Paradox

setosa.io

why Angular and D3?

resuable components through
directives



          

let's be honest...

Angular's bi-directionally bound isolate scoped, transcluded, element restricted directives

===

it ain't that bad :)

src: @johnkpaul

directives are awesome!

source: http://alicialiu.net/leveling-up-angular-talk/#/1

the plan

  • svg intro
  • selections
  • scales
  • axis
  • transitions
  • basic tooltips
  • angular integration
  • mobile friendly

svg intro

svg


            
          
(500, 300) (0, 0)

rect


            
          
(500, 300) (0, 0)

circle


            
          
(500, 300) (0, 0)

ellipse


            
          
(500, 300) (0, 0)

text


            Hello SVG!
          
Hello SVG! (500, 300) (0, 0)

line


            
          
(500, 300) (0, 0)

polyline


            
          
(500, 300) (0, 0)

path


            
          
(500, 300) (0, 0)

check this out too

https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial

and this

Leaving Pixels Behind

let's build a bar chart

Top GitHub Languages for 2013

var data = [
  { lang: 'JavaScript', value: 549385},
  { lang: 'Ruby', value: 453004},
  { lang: 'Java', value: 375857},
  { lang: 'PHP', value: 278937},
  { lang: 'Python', value: 247099},
  { lang: 'C++', value: 177001},
  { lang: 'C', value: 167175},
  { lang: 'CSS', value: 105897},
  { lang: 'C#', value: 76874},
  { lang: 'Objective-C', value: 75399},
  { lang: 'Shell', value: 70516},
  { lang: 'Perl', value: 47954},
  { lang: 'CoffeeScript', value: 27402},
  { lang: 'Go', value: 23334}
];
          

Data source: GitHub Archive

Setup

you only need d3.js

+

some styling

Margin Convention

// (1) Select element
var el = d3.select('#chart'),

// (2) Grab element's dimensions
elWidth = parseInt(el.style('width'), 10),
elHeight = parseInt(el.style('height'), 10),

// (3) Declare margins for axis
margin = {top: 20, right: 10, bottom: 80, left: 40},

// (4) calculate width and height used for scaling
width = elWidth - margin.right - margin.left,
height = elHeight - margin.top - margin.bottom;

// (5) append svg element with added margins and group element inside
var svg = el.append("svg")
  .attr("width", elWidth)
  .attr("height", elHeight)
.append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")")

// now in our code we can just reference width and height
          
http://bl.ocks.org/mbostock/3019563

Selections

d3.select


var el = d3.select('#chart');

// append an svg element
var svg = el.append('svg');

// append returns a new selection
svg.attr(...)
          

One element selected, one appended

d3.selectAll


// with many selected, can change all elements
var rect = svg.selectAll('rect')
                .attr(..);
          

Let's talk about data

Data are arrays


var data = [1, 3, 5, 7, 9];

var meetups = [
  { name: 'AngularJSOC', members: 286 },
  { name: 'OCMongoDB'  , members: 326 }
];
          

Data are mapped to elements


var selection = svg.selectAll('rect')
                  .data(data); // this creates empty placeholders

// appending to the enter selection creates new elements
selection
  .enter()
  .append('rect')
    .attr(/*...*/)
          

Enter, Update & Exit

Three Little Circles

Thinking with Joins

Things to remember

  1. If the new dataset is smaller than the old one, the surplus elements end up in the exit selection and get removed.
  2. If the new dataset is larger, the surplus data ends up in the enter selection and new nodes are added.
  3. If the new dataset is exactly the same size, then all the elements are simply updated with new positions, and no elements are added or removed.

source: Thining With Joins

We can chain

// create viz
var selection = svg.selectAll('rect')
  .data(data)
  .enter()
  .append('rect')
    .attr({
      x: function (d, i) { ... },
      y: function (d, i) { ... },
      height: function (d) { ... },
      width: ...,
      fill: ...
    });
          

Some useful
D3
functions

d3.max


var values = [2, 10, 3];
var max = d3.max(values); // 10

// if array of objects...
var maxRepoCount = d3.max(data, function (d) {
  return d.value;
});
          

d3.extent


var values = [3, 1, 5, 8, 9, 2];

var max = d3.extent(values); // [1, 9]
          

Scales

d3.scale


var heightScale = d3.scale.linear()
  .domain([0, d3.max(data, function (d) { return d.value})])
  .range([height, 0]);

// ordinal scales for discrete data
var xScale = d3.scale.ordinal()
  .domain(data.map(function (d) { return d.category; }))
  .rangeRoundBands([0, width], 0.1); // useful for barcharts
          

Use scales for rect's attr

               
selection
  .attr({
    x: function (d, i) {
      return xScale(d.lang)
    },
    y: function (d, i) {
      return yScale(d.value);
    },
    height: function (d) {
      return height - yScale(d.value);
    },
    width: xScale.rangeBand(),
    fill: fillColor
  })
                
             

demo

Axis

d3.svg.axis


// create axis
var xAxis = d3.svg.axis()
  .scale(xScale)
  .orient("bottom") // place label below tick
  .tickPadding(10); // padding b/n tick and label

var xAxisGroup = svg.append("g")
  .attr({
    class : 'axis',
    transform: 'translate(' + [0, height] + ')'
  }).call(xAxis);

          

axis demo

Transisitions

General Pattern

// (1) initial attributes
var r = svg.selectAll('rect')
  .data(data)
  .enter()
  .append('rect')
    .attr(initialAttrs);

// (2) transition to final state
r.transition()
  .delay(function (d, i){
    return d * 25; // delay each element
  })
  .ease("linear") // also "elastic", "bounce", etc.
  .duration(700) // entire transition
  .attr(finalAttrs);
          

transition demo

transition demo 2

Custom Tooltips

selection.on(type, listener)


var r = svg.selectAll('rect')
  .data(data)
  .enter()
  .append('rect')
    .attr(...)
    .on('mouseover', handleMouseover);

function handleMouseover(d, i) {
  // this: element moused over
  // d: datum
  // i: index
}
          

tooltip.css


.tooltip {
  visibility: hidden;
  background-color: #39393d;
}

.tooltip text {
  fill: #fff;
  font-size: 12px;
  shape-rendering: crispEdges;
}
          

add tooltip to svg


var tooltip = svg.append('g').attr({class: 'tooltip'});

tooltip.append('rect').attr({ height: '30', width: '100' });

tooltip.append('text').attr({ x: 10, y: 20 }); // relative to group element
          

event handlers


function handleMouseover(d, i) {
  // calculate x, y (add code to check for boundaries)
  var pos = { x: xScale(d.lang), y: yScale(d.value) - 35};

  var tooltip = svg.select('.tooltip')
    .attr('transform': 'translate(' + [pos.x, pos.y + ')');

  tooltip.select('text').text('Repos: ' + d.value);

  tooltip.style('visibility', 'visible');
}

function handleMouseout(d, i) {
  svg.select('.tooltip').style('visibility', 'hidden')
}
          

tooltip demo

tooltip demo 2

Let's bring in our Angular chops

http://goo.gl/nkFq9l

Step 1: Get Data from a service

'use strict';

angular.module('d3AngularDemosApp')
  .controller('BarChartCtrl', ['$scope', 'DataSvc',
    function ($scope, DataSvc) {

      $scope.ui = {};

      $scope.refresh = function () {
        $scope.ui.topRepos = [];
        DataSvc.getTopRepos()
          .then(function (data) {
            $scope.ui.topRepos = data;
          });
      };

      $scope.refresh();
    }
  ]);

          

Step 2: basic directive

angular.module('app')
  .directive('barchart', function () {
    function linker(scope, element, attrs) {
      // set up all the  components (svg, scales, axis, .etc)
      var el = element[0];
      ...
      var svg = d3.select(el).append('svg')
      ...
      // exclude setup code that needs the data
    }

    return {
      template: '
', restrict: 'E', replace: true, link: linker, scope: { // isolate scope data: '=', // bi-directional data binding x: '@', // the x property (string) y: '@' // the y property (string) } }; });

Step 3: "watch" for data


scope.$watch('data', function (newData, oldData) {
  var data = angular.copy(newData); // let's make a copy
  scope.render(data);
}, true); // watch for object equality
          

Step 4: scope.render


scope.render = function (data) {
  // (1) update scales, axis
  // (2) transition new data
  var rect = svg.selectAll('rect').data(data);
  rect.enter().append('rect').attr(/*...*/);
  rect.transition().attr(/*...*/);
  // (3) remove any data not needed
  rect.exit().remove();
}
          

Step 5: watch for resize event


// Browser onresize event
window.onresize = function() {
  scope.$apply(); // launch a digest cycle
};

// resize chart when the width changes
scope.$watch(function () {
  return el.clientWidth;
}, function () {
  scope.resize();
});
          

Step 6: scope.rezize


scope.resize = function () {
  // (1) update width
  width = el.clientWidth - margin.right - margin.left;

  // (2) update svg
  var svg = d3.select(el).select('svg');
  svg.attr('width', width + margin.right + margin.left);

  // (3) update everything that is related to width
}
          

Step 7: Directive usage



          

directive demo

Also check this awesome examples on directives

Building Custom Directives

D3 Angular Directive Intro

Some extra Resources

and check this playground out:

victormejia.github.io/d3-angular-demos

pull requests are welcome :)

the end :)

© 2014 PEANUTS Worldwide LLC