Best practices for creating reusable visualizations - D3 on AngularJS: Create Dynamic Visualizations with AngularJS (2014)

D3 on AngularJS: Create Dynamic Visualizations with AngularJS (2014)

Best practices for creating reusable visualizations

So far our visualizations have been relatively simple example visualizations mainly intended to help us understand how Angular and D3 works. In the the wild, our visualizations are often drastically more complicated. This chapter will cover some of the general best practices the authors have come across when creating directives that are general purpose and reusable.

warning

It’s important to remember that over generalization can be just as bad as under generalization. A good strategy is to always take some time up front to consider if a visualization needs to be generalized. Does the visualization need to be used in multiple places? If the answer is “no” then some of these techniques may unnecessarily over complicate our code.

Accessor functions

In Angular, a scope expression is a code snippet, similar to a function, that can be configured on the directive as an attribute. Our directive will evaluate this expression when accessing each element in our data. By making the accessors configurable, we avoid having a directive that assumes a particular input format.

This means our directive can accept any type of data array so long as we tell it out to pluck the specific values out of the data using the accessor expression. We’ll walk through a demonstration of what this means assuming our data is the following viewership information for AMC’s The Walking Dead TV series.

1 scope.episodes = [

2 { title: 'Days Gone Bye', air_date: '2010-10-31', us_viewers: 5.35 },

3 { title: 'Guts', air_date: '2010-11-07', us_viewers: 4.71 },

4 // ...

5 ];

We could specify the X and Y accessor expressions for a scatter plot directive given that our directive has two Angular expressions on its scope, accessorX, and accessorY using the following.

1 <scatter

2 data="episodes"

3 accessor-x="d.air_date"

4 accessor-y="d.us_viewers">

5 </scatter>

We can do this using the & when configuring the directive. To make the two attributes accessor-x and accessor-y expressions, the scope configuration on our directive needs to use an & instead of a =.

1 scope: { accessorX: '&', accessorY: '&', data: '=' }

The last step is to use the scope expressions when accessing the x and y values of the data array in the directive.

1 var d = data[0];

2 var xVal = scope.accessorX({d: d});

Using the same syntax, we can add additional variables that will be available inside the expression. In this way, we could expose the index variable i in the expression or any other variables we’d like to be accessible from with the accessor.

1 var i = 0;

2 var d = data[i];

3 var xVal = scope.accessorX({d: d, i: i});

This technique also works well for other properties, like color, size, or anything else we’d like to be made configurable. This also makes it convient to set a property uniformity. In the case of our scatter plot, we might make the color configurable by an expression.

1 <scatter data="episodes" color="color(d.title)" ... ></scatter>

Inside of our controller, we would then need a color scale that the expression uses to determine the color for each plot point.

1 // on our controller

2 $scope.color = d3.scale.category10();

Or alternative, set all the points to be "red" by setting color="'red'" in the accessor expression.

1 <scatter data="episodes" color="'red'" ... ></scatter>

error

Live version: http://bl.ocks.org/vicapow/9591312

Responsive directives

Responsive web development involves having the components of a site automatically resize or hide to take up just enough of the screen space as is available. Browsing a responsive site on a mobile phone with a smaller screen would show the site in an adapted view optimized for the new screen dimensions. Similarly, for larger screens, a responsive site will grow large enough, possibly showing more content, to fully utilize the larger space. This is alternative to having two or three separate versions of the same site for different screen sizes. With a responsive website design, the site grows or shrinks dynamically in “response” to screen size changes.

A good technique for having directives update dynamically depending our the size of the screen is to have the directive watch for changes to its width and height and update any internal content within the visualization accordingly.

1 var w = 0;

2 var h = 0;

3 scope.$watch(function(){

4 w = el.clientWidth;

5 h = el.clientHeight;

6 return w * h;

7 }, resize);

But for this to work as expected, we’ll need to have the scope check for changes every time the window resizes.

1 angular.element($window).on('resize', function(){ $scope.$apply() });

A good place for this code is somewhere inside a main controller because it only needs to be called once. All directives that are made responsive in this way, will be checked for changes once this one line is added somewhere within our application.

The last step is to make sure the directive element can be shrunken down. The containing element will never we smaller than its content unless we set overflow:hidden meaning, “ignore any content inside this element that goes beyond the element itself.”

1 scatter{

2 display: block;

3 overflow: hidden;

4 }

Now, to set the actual size of the directive, we can use CSS percentages to have the size of the visualization update dynamically just like any other built in HTML element.

1 .scatter-1{

2 width: 50%;

3 height: 400px;

4 }

Or to have it take up the entire page:

1 .scatter-1{

2 width: 100%;

3 height: 100%;

4 }

error

Live version: http://bl.ocks.org/vicapow/9716306

Services

Services to store data

Beyond having directives remain unopinionated about their data sources, it’s also a good idea to do the same for controllers. This allows us to easily swap out different types of data from different controllers acting as a proxy between the controller and the data. Here’s a concrete example for when this ability would be useful. Imagine we have two controllers that both need to access the same remote piece of data. Instead of having each controller request the remote data independently, issuing two separate requests, we can instead use an Angular service to provide the data to each controller. In this way, the service can automatically store the data internally for subsequent requests or provide any necessary data formatting prior to passing the data back to each controller.

Services to reduce code duplication

Services are also a great place to store common utility functions that multiple controllers might each need. This reduces the amount of code duplication across controllers. But services aren’t limited to just being used inside of controllers. Directives can also use services. Anytime we notice duplicate or similar code across directives or controllers, the best place to share this code is from within a service.

Built in directives

To keep things simple, up to this point we’ve avoided creating our own directives in any other way but by their element name. That is, if we wanted a map component we created an element style directive and used it in our HTML as <my-map>. But directives are actually much more flexible and their use isn’t limited to just that. Directives, in actuality, are just chunks of code that run on a DOM element. We’ve been using them to encapsulate complex DOM manipulations (our visualizations) while exposing a simple interface (their use in HTML as just <my-map>) but directives can also be created using the an attribute syntax such as <div my-map></div>. This is done be by using restrict: 'A' instead of restrict: 'E' in our directive definition. Most of the features introduced to newcomers of Angular are just the most useful directives that come bundled with the Library.

One of the exciting things about using directives as attributes is that we’re no longer limited to having just one directive on an element. All the directives on a given element will have access to the same scope properties. Take the ng-show directive which either hides or shows the element its attached to depending on the evaluation of its expression as true or false. This makes it easy to implement something like a “loading” dialog. With it we can create a directive component that is the loading dial and use the ng-show directive to hide the directive once a map (or any other component) has finished loading its data.

1 <loading-overlay ng-show="mapIsLoading">

2 </loading-overlay>

3 <my-map loading="mapIsLoading"></my-map>

error

Live version: http://bl.ocks.org/vicapow/9819505

Using replace, template, and transclude to modify the behavior of our visualizations

When defining directives, we can optionally tell the Angular compiler that it should replace the element with the directive with a provided template. This comes in handy if we want to replace the custom directive element with an actual SVG element instead of having a custom directive element “wrapping” our visualization (which is what our directives have been doing up until now.) Let’s walk through an example of this in the case of creating a sparkline directive. Sparklines are little mini graphs that can be embedded inline between words in a text document.

Making a sparkline directive has a few advantages. The most noticeable is how easy it is now to create more just by using this self explanatory HTML.

1 This is a sparkline <sl r="5">[20,24,15,40]</sl>. That was a sparkline.

error

Live version: http://bl.ocks.org/vicapow/9904612

information

We decided to use sl as the directive name because we anticipated having to use many of them within a body of text. It would have been tedious to type out fully <sparkline>...</sparkline> for each spark line.

The drawback to defining our directive in this is way that we’re not able to simply specify the styles on the directive itself however, especially with sparklines, we’ll often want to style each one individually inline.

1 This is a red sparkline <sl style="stroke:red">[20,24,15,40]</sl>

This would not work as expected because the sl DOM element isn’t an SVG DOM element and so its stroke property wont be inherited by the containing SVG element that our directive appends to itself. To fix this, we can tell Angular to instead replace the entire element with an SVG element using the replace: true option when we define the directive. But for this to work, we also need to tell Angular what it would replace the element with. To do this we’ll also specify a template

1 return {

2 link: link,

3 restrict: 'E',

4 replace: true,

5 template: '<svg class="sl"></svg>'

6 }

Angular will carry of any styles or attributes be specified on the original directive to the replaced directive.

But this alone wont be enough. As our code is written now, Angular will replace the data we put inside of the <sl> element. To tell Angular, “hey, instead of removing all the stuff inside of the element before compiling it, put it inside of here” we can use the transclude directive option in combination with the ng-transclude directive inside of our template.

1 return {

2 link: link

3 , restrict: 'E'

4 , replace: true

5 , template: '<svg ng-transclude class="sl"></svg>'

6 , transclude: true

7 };

Whatever was in our <sl>...</sl> directive before will be placed inside of the <svg> element created from this template string.

And now styles can be applied inline on our <sl> directive to adjust the display of the sparklines.

1 <sl r="1" style="stroke:green">[10, 20, 30]</sl>

error

Live version: http://bl.ocks.org/vicapow/9