English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

Developing a D3.js Edge Special Code

2. API Requirements

  • Definition of an API
  • Different meaning of the world "modular"
  • Requirements for our API
  • Some implementation details 

Most D3.js examples show how to build a single standalone chart. In the previous chapter we saw an example of a typical D3.js chart and saw an illustration of some of the issues that can be encountered when we look to reuse a chart multiple times on the same page. In a real-world application, these charts need to interact with a lot of different technologies.

In this chapter, we will see what the requirements are for integrating D3.js charts in an application, and how the reusable API is a good fit for the task.

What's an API?

An Application Programming Interface (API) is "a specification of how some software components should interact with each other." For example, the code can be built as multiple modules exposing some public functions that the other modules can use, as part of a contract between them. We will refine this terminology to better explain the requirements for our API in the contect of D3.js.

For chart code to be reusable, we need to easily instantiate a new chart independent from the other instances. We don't want the properties of one chart propgating to other instances. For this requirement, we will use a chart generator, which is a function returning a new chart object with the configuration we want (for example, a function returning a function). This will ensure that the context of all the settings and methods are relative to the instance of the chart, rather than all charts. 

To set and modify individual chart settings, we will use getter and setter functions instead of accessing inner variables directly. Accessing configuration variables through a function has multiple advantages. First, the function can validate the arguments, change the type, compute some side effects, and so on. Second, the inner variable can change name or even be removed, but the getter and setters functions will stay consistently the same. That's the best example of what an API is all about: the interface with your code is consistent and predictable even if the internal implementation is deeply refactored. This ensures that any applications built using your API, including your own applications, don't break if you modify the internals of your code. 

Refactoring would be very difficult without a good API. Professional code always uses some kind of unit testing to insure that the API is clear with integration tests, and to make sure that the interaction is constantly respected between modules. The unit tests are mainly a set of expectations on the behavior of a module--of a unit of code. It's easier to aggressively refactor the implementation of a module when the only thing that can't change is the API. The test suite will then quickly identify the part of the code that breaks the contract. 

Each chart should be self-contained and decoupled from the others. For example, a chart can ask for a dataset in a specific format without needing to know about the file format coming out of a data manager module instead of having a data manager hardcoded in it. Good modules clearly separate the functionalities. In our example, the chart module takes care of building the chart and rendering it while the data manager handles the dataset request and persistence. If it depends upon other modules, the dependency could be injected instead of hardcoded. The separation of concern between a model, a view and a controller is a well-known pattern. An MV* library is often used for adding this structure to a whole application, handling dependencies (require.js), and communication between modules (AngularJS two-way bindings), and so on. Integrating a D3.js chart in an application is often a matter of building a self contained D3.js chart module that will play nice with all of the other components. 

D3.js modular API

In this book, we are mainly concerned with D3.js modules interacting with other D3.js modules. D3.js modules can be of different types. You can build your own chart library as a collection of chart modules. But other pieces of code can be wrapped under a module. We will call a "component" a reusable piece of code generating graphics, which doesn't represent a complete chart, but a part meant to be composed with other elements. D3.js is built out of components. You use these components everytime you generate a visualization with D3.js! The best example is d3.svg.axis. You can draw an axis, but they will be more useful as part of a chart. Components are building blocks, and they are a good illustration of how D3.js will prefer composition to inheritance. 

Other modules don't generate graphics. For example, "layouts" like d3.layout are preparing data for the graphics space, and injecting some abstract position and size information that is used to map from data space to pixel space. Other modules, called "generators," take some data as input and return an SVG string as output. These strings are not graphics element until they are used as SVG attributes and bound to the DOM. Some more modules take care of the non-graphical stuff, like data helpers. d3.nest or d3.extent are good examples of D3 helpers.

All of these packages can be shared as plugins, and are modules you can use to add functionalities to the core functions. Most plugins you will find on the web are complete charts, with a lot of them using the Reusable API. But some non-graphics plugins are also available, like keybinding, graph data manipulation or svg transform. Plugins are a very important part of the D3.js ecosystem. The core D3.js code will probably not expand a lot, in fact, it may even tend to shrink. After all, D3.js is "a visualization kernel" and some plugins and chart libraries are growing in popularity. One of the best examples is the way the map plugin is growing--counting more than 80 projections at the time of writing. 

Implementation Overview

Multiple modules can live under a common namespace, to be clearly identified as related, and to expose a single variable to the global namespace. For example, all D3.js code is under the d3 namespace (i.e., d3.select, or d3.svg.axis). In this book, we will add our own namespace to the d3 one:

d3.edge = {};

A module should be self-contained and expose a public API. One way for encapsulating the code is to wrap it inside a simple function:

d3.edge.simpleChart = function(){ /*chart code*/ };

The Reusable API uses a neat trick to expose public functions while keeping others private, using an outside function just for exporting an inner function, and using getters and setters to give access to "private" variables that are in the closure:

d3.edge.simpleChart = function(){
    var height = 100;
    function exports(){
        /*chart code*/
    }
 exports.width = function(_x) {
 width = _x;
 return this;
    };
 return exports;
};

This snippet of code is pretty dense and is the core concept of the Reusable API that we will explain and illustrate throughout this book. More specifically, we want to focus on how this pattern helps to fulfill our API requirement. So let's start by listing these requirements.

API requirements

To summarize, we want a modular API. But what does it mean in our own words?

Namespaced: only a single object is exposed to the global scope, preventing name clashing and clearly identifying relationships.

Encapsulated: one way of encapsulating code is to wrap a function around it (simple, IIFE, etc.) to hide a certain amount of code under a simple abstract syntax (i.e., d3.edge.pieChart()).

Decoupled: a module doesn't know about the others. It only handles it's own internal state and behavior, exposing an API for others to use. Dependencies can be handled in multiple ways (i.e., dependency injection).

Consistent: the API never changes, the pattern is clear and the naming is significant. 

Composable: preferring composition to inheritance can be really hard when you come from an Object Oriented (OO) background. You can find plenty of resources and debates on the web about prototypal inheritance and other features. Let's just say here that studying D3.js source code is a good way to convince yourself that the functional aspect of Javascript can be pretty powerful. In D3.js, you don't write a "chart" code and then derive a "bar chart" from it. The typical bar chart rather is a composition: an assembly of rectangles, texts, axes, and other modules. 

The Reusable API also has some more interesting characteristics. For example, when you update a d3.svg.axis configuration, or when you bind new data to your selection, you simply call the axis again without having to explicitly call a draw or an update method. 

svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

//Update
x.domain(d3.extent(data));
d3.select(".x.axis")
    .call(xAxis)

Another interesting feature is that setters also act as getters when no arguments are provided. D3.js also provides interesting ways of exposing events using d3.dispatch and d3.rebind. There is a lot to learn from D3.js's internal structure. Learning the Reusable API is a good pretext to do so.

Summary

The Reusable API is a variant of a module pattern. Writing modular code can mean a lot of different things. We tried to list some features a modular API should have. With this list of requirements and at least an approximate terminology, you are ready to start learning the Reusable API, how to implement and test it, and use it in a real-world example of integrating D3.js charts in an application.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.