Skip to content

Latest commit

 

History

History
110 lines (83 loc) · 4.02 KB

README.md

File metadata and controls

110 lines (83 loc) · 4.02 KB

AngularD3Showcase

###This is an AngularJS application that showcases a variety of techniques and patterns, including:

  • Integrating D3 with Angular
  • Creating D3 components using TDD
  • Creating reusable charts using the Revealing Module pattern and a little functional programming
  • Mocking with sinon.js
  • Dispatching custom events from D3 components
  • Wrapping D3 components in Angular Directives
  • Injecting mock services into Directives
  • Nesting directives with transclusion
  • Testing event handlers
  • Inter-Directive communication using events and shared scope (a basic Mediator pattern)
  • and more

Getting Started

Assuming you have npm, bower, and grunt installed, the following commands should get you up and running:

npm install
bower install
grunt serve

At that point, you should see something like this in your browser:

screenshot

To run the tests using PhantomJS...

grunt test

Reusable Grouped Bar Chart component

I wanted to create a reusable D3 grouped bar chart and wrap it in a service so I could inject that service into a custom directive. I could have chosen to use d3 directly from a directive, but injecting a service allows me to mock that service when I test my directive.

I developed my D3Service using TDD. The spec is here:

d3ServiceSpec.js

and the service is here:

d3Service.js

I wanted users of the chart to be able to pass in their own functions for some of the logic that the chart needs to perform on their data. Here is a snippet showing the setters on the reusable chart module:

exports.yValueFunction = function(f) {
    if (!arguments.length) { return yValue; }
    yValue = f;
    return this;
};

exports.yMaxFunction = function(f) {
    if (!arguments.length) { return yMax; }
    yMax = f;
    return this;
};

Then, when I'm calculating the max for my d3.scale.ordinal.domain I can call provided yMax function (or use the default I provide):

y.domain([0, yMax(data)]);

There are a bunch of tests in the spec, but ultimately I count the groups and bars to make sure they match the data passed in:

it('should create correct number of groups and bars', function() {
    var f = fixture.datum(dataSet).call(barChart);

    //use some functional programming to count the nodes in dataSet
    var nodeCount = _.flatten(_.map(dataSet, function(d){ return d.nodes})).length;

    expect(fixture.selectAll('.g')[0].length).toBe(dataSet.length);
    expect(fixture.selectAll('.bar')[0].length).toBe(nodeCount);
});

##Communication between the chart and a directive, and between directives After I had the chart looking the way I wanted, I added some interactivity. The chart exposes a custom event called 'barHover' which I listen for in a tbNodeChart directive:

Which, in turn, dispatches an event up to its parent scope:

barChart.on('barHover',function(obj){
    $scope.$emit('itemHover', obj);
});

Tests for this directive use Sinon.js for mocking the d3Service mentioned above. See tbNodeChart.spec.js

In chartPodController.js I test to make sure the event is handled correctly:

it('should respond to an itemHover event by updating hoveredItem', function(){
    var mockHoveredItem = {};
    $controllerConstructor('ChartPodController', {$scope:scope});
    rootScope.$broadcast('itemHover', mockHoveredItem);
    expect(scope.hoveredItem).toBe(mockHoveredItem);
});

The tbChartPod directive then updates its display to show the value of the currently hovered bar.

##More coming soon...