Contents


Use D3 and Angular for custom data visualizations

Create reusable visualizations for your JavaScript apps

Comments

Data is everywhere. Knowing what to do with the data that you have is key. Data visualizations present relevant data to users in a concise, consumable way that can help them to derive trends.

For the web, plenty of libraries are out there to help you produce the common types of visualizations: line charts, bar charts, pie charts, and so on. But sometimes you need to present data in a novel way, and it's up to you to create this new visualization. D3.js— a flexible JavaScript library for data-driven manipulation of documents — can help you build custom visualizations. D3.js doesn't offer any built-in visualizations; it gives you the building blocks to construct your own. The result is often in Scalable Vector Graphics (SVG) format, but it's entirely possible to bind regular old HTML to your data.

In this tutorial, I show how to incorporate D3.js into your AngularJS applications. (My code examples use ECMAScript 2015 as the JavaScript version, but it's not required for them to work.)

Sometimes you need to present data in a novel way, and it's up to you to create this new visualization.

Making a data list with D3

This D3 code creates an unordered list and adds three list items: angular, d3, and c3:

d3.select('body')
  .append('ul')
  .selectAll('li')
  .data(['angular', 'd3', 'c3'])
  .enter()
  .append('li')
  .text(d => d);

Here's the rendered visualization:

Screenshot of the rendered data list

Making a visualization based on data weight

The following D3 code creates an SVG visualization that contains three circles whose radii and offsets are based on the specified values:

d3.select('body')
    .append('svg')
    .selectAll('circle')
    .data([1000, 10000, 250000, 15000])
    .enter()
    .append('circle')
    .attr('r', d => Math.log(d))
    .attr('fill', '#5fc')
    .attr('stroke', '#333')
    .attr('transform', (d, i) => {
        var offset = i * 20 + 2 * Math.log(d);
        return `translate(${offset}, ${offset})`;
    });

And here's the visualization:

Screenshot of the bubbles visualization

Wrapping a visualization in an Angular directive

Any time that you manipulate the DOM with Angular, it's best to do so in a directive. Putting an Angular module around custom data visualizations is a good way to keep things grouped together for future use.

This code creates a new directive that can be instantiated by adding <bubbles></bubbles> to the markup:

angular.module('pi.visualizations').directive('bubbles', bubbles);
function bubbles() {
    return {
        restrict: 'E',
        controller: () => {},
        bindToController: true,
        controllerAs: 'viz'
    };
}

I'm using bubbles as the directive name so I can use the example from the "Making a visualization based on data weight" section. At the moment, the directive isn't really accomplishing anything, so I need to add some implementation details.

The directive can accept an array of data points for the visualization to use. I add the array as a property of the directive, via the scope property, where it'll now bind a values property to the controller. Also, since the visualization will be done as an SVG, it might as well serve as the template for the visualization, which I set through the template property:

angular.module('pi.visualizations').directive('bubbles', bubbles);
function bubbles() {
    return {
        scope: {
            values: '='
        },
        template: '<svg width="900" height="300"></svg>',
        restrict: 'E',
        controller: () => {},
        bindToController: true,
        controllerAs: 'viz'
    };
}

The directive's link function is where the work of using D3 to generate the chart will take place.

Liberating your visualization

You can inline everything into the link function — but the code has no particular dependency on Angular, so it might be more beneficial to build the visualization logic as a stand-alone component. This approach promotes the idea of loose coupling, and it frees this visualization for other projects that might not use Angular.

In this example, I use CommonJS to include a class that's responsible for rendering the visualization:

// There is a dependency on d3 for the visualization
var d3 = require('d3');

// A class that renders values on a logarithmic scale
function Bubbles(target) {
    this.target = target;
}

// Does the work of drawing the visualization in the target area
Bubbles.prototype.render = function (values) {
    d3.select(this.target)
        // Get the old circles
        .selectAll('circle')
        .data(values)
        .enter()
        // For each new data point, append a circle to the target SVG
        .append('circle')
        // Apply several style attributes to the circle
        .attr('r', d => Math.log(d))
        .attr('fill', '#5fc')
        .attr('stroke', '#333')
        .attr('transform', (d, i) => {
            // This moves the circle based on its value
            var offset = i * 20 + 2 * Math.log(d);
            return `translate(${offset},${offset})`;
    });
};

// Does any cleanup for the visualization (e.g., removing event listeners)
Bubbles.prototype.destroy = function () {}

// Exports the visualization
module module.exports = Bubbles;

To use the class:

var Bubbles = require('./Bubbles');
// The target SVG element
var svg = document.getElementById('my-visualization');
var visualization = new Bubbles(svg);
visualization.render([1000, 25000, 3000000, 120000, 25, 10203]);

Tying the class into the directive

I have a directive that creates an SVG and a class that renders the data. Now the two need to be tied together. As I mentioned earlier, this work happens in the directive's link function:

angular.module('pi.visualizations').directive('bubbles', bubbles);
function bubbles() {
    return {
        scope: {
            values: '='
        },
        template: '<svg width="900" height="300"></svg>',
        restrict: 'E',
        controller: () => {},
        bindToController: true,
        controllerAs: 'viz',
        link: function (scope, element, attrs, ctrl) {
            // Bring in the Bubbles class
            var Bubbles = require('./Bubbles');
            // Create a Bubbles visualization, targeting the SVG element from the template
            var visualization = new Bubbles(element.find('svg')[0]);
            // Watch for any changes to the values array, and when it changes, re-render the chart
            scope.$watchCollection(() => ctrl.values, () => {
                visualization.render(ctrl.values ? ctrl.values : []);
            });
            scope.$on('$destroy', () => {
                // If we have anything to clean up when the scope gets destroyed
                visualization.destroy();
            });
        }
    };
}

And this code renders the visualization in the application:

<bubbles values="[1000, 25000, 3000000, 120000, 25, 10203]"></bubbles>
Screenshot of the final bubbles visualization
Screenshot of the final bubbles visualization

Planning for the future: Angular 2

Angular 2 establishes a new way of developing components. Some aspects will be familiar and others drastically different. Ultimately, you can follow the same process that I've shown you to produce reusable data visualizations in your Angular 2 applications. The code in this section is in TypeScript, which is a popular language choice for Angular 2.

As before, I separate the Angular-specific code and the visualization code into separate modules, putting the visualization code in bubbles.chart.ts. Functionally, there's not much difference between the TypeScript and the vanilla JavaScript code:

// There is a dependency on d3 for the visualization; can be included as
// <script src="//d3js.org/d3.v2.js"></script>
declare var d3;

// Exports the visualization module
export class BubblesChart {
    target: HTMLElement;
    constructor(target: HTMLElement) {
        this.target = target;
    }

    render(values: number[]) {
        d3.select(this.target)
        // Get the old circles
        .selectAll('circle')
        .data(values)
        .enter()
        // For each new data point, append a circle to the target SVG
        .append('circle')
        // Apply several style attributes to the circle
        .attr('r', d => Math.log(d))
        .attr('fill', '#5fc')
        .attr('stroke', '#333')
        .attr('transform', (d, i) => {
                // This moves the circle based on its value
                var offset = i * 20 + 2 * Math.log(d);
                return `translate(${offset}, ${offset})`;
        });
    }

    destroy() {
    }
}

The biggest change is that Angular 2 includes several classifications of directives. I need a component directive here, because it's a directive with a template. This component is in bubbles.component.ts:

// Loads some required modules from Angular.
import {Component, Input, OnChanges, AfterViewInit, ViewChild} from '@angular/core';
// Loads the code needed to manipulate the visualization
import {BubblesChart} from './bubbles.chart';

// Identifies the class as a component directive that will be associated
// with `bubbles` elements in the DOM, and will include the specified markup as its template
@Component({
    selector: 'bubbles',
    template: '<svg #target width="900" height="300"></svg>'
})
export class Bubbles implements OnChanges, AfterViewInit {
  // Declares values as a data-bound property
    @Input() values: number[];
  // Gets a reference to the child DOM node
    @ViewChild('target') target;
  // An instance of the BubblesChart
    chart: BubblesChart;

    constructor() {
    }

  // Lifecycle hook that is invoked when data-bound properties change
    ngOnChanges(changes) {
        if (this.chart) {
            this.chart.render(changes.values);
        }
    }

  // Lifecycle hook for when the component's view has been fully initialized
    ngAfterViewInit() {
    // We have to wait until the view has been initialized before we can get the
    //DOM element to bind the chart to it
        this.chart = new BubblesChart(this.target.nativeElement);
        this.chart.render(this.values);
    }

}

In Angular 2, to include one component in another, you must add it to the directives array property of the containing component's metadata. The following code enables the bubbles chart to be rendered in the my-app component:

import {Component} from '@angular/core';
import {Bubbles} from './bubbles/bubbles.component';

@Component({
    selector: 'my-app',
    template: '<bubbles [values]="[1000, 25000, 3000000, 120000, 25, 10203]"></bubbles>',
  directives: [Bubbles]
})
export class MyApp {
  constructor() {
  }
}

Conclusion

I've shown you a simple example of creating reusable, custom data visualizations by using D3.js and Angular or Angular 2. From here, you can add more properties to your directive to provide more configuration over the visualization, and then update the render function so that it accurately represents the data set that's provided to it.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development, Open source, Big data and analytics
ArticleID=1031984
ArticleTitle=Use D3 and Angular for custom data visualizations
publish-date=05262016