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

Developing a D3.js Edge Special Code

3. The Reusable API

  • A walkthrough of a "Hello World" reusable component
  • A discussion of namespace, reusable module, closure, getters and setters

In the previous chapter we defined what the words module and plugin mean, at least in the context of JavaScript, D3.js and this book. We also defined what we mean when we say something is a reusable function, component or chart. We also referred to the Best Practices for D3.js as described by Mike Bostock in "Towards Reusable Charts". The principles outlined in that post are used in the D3.js core as well.

In this chapter we take a look at the implementation of these concepts in the form of a very simple "Hello World" plugin.

Hello World

We will use a very simple "Hello world" plugin that just adds a <div> with text that you can hover over with the mouse to see a message in the console. In later chapters you will see the API at work in more detail--for example, how to generate animated transitions and test using a unit test suite.

The next chapter has a complete real-world example to illustrate this pattern in practice, using the bar chart from Chapter 1 ("Standard D3").


  Access the source code: code/Chapter03/ 


Here is the complete code we will describe. If you are already familiar with this pattern, feel free to jump directly to the line you want to learn more about:

d3.edge = {};

d3.edge.table = function module() {
    var fontSize = 10,
        fontColor = 'red';

    // To get events out of the module
    // we use d3.dispatch, declaring a "hover" event
    var dispatch = d3.dispatch('customHover');
    function exports(_selection) {
        _selection.each(function(_data) {
            d3.select(this)
            .append('div')
            .style({
                'font-size': fontSize + 'px',
                color: fontColor
            })
            .html('Hello World: ' + _data)
            // we trigger the "customHover" event which will receive
            // the usual "d" and "i" arguments as it is equivalent to:
            //   .on('mouseover', function(d, i) {
            //       return dispatch.customHover(d, i);
            //   });
            .on('mouseover', dispatch.customHover);
        });
    }
    exports.fontSize = function(_x) {
        if (!arguments.length) return fontSize;
        fontSize = _x;
        return this;
    };
    exports.fontColor = function(_x) {
        if (!arguments.length) return fontColor;
        fontColor = _x;
        return this;
    };
    // We can rebind the custom events to the "exports" function
    // so it's available under the typical "on" method
    d3.rebind(exports, dispatch, "on");
    return exports;
};

// Setters can also be chained directly to the returned function
var table = d3.edge.table().fontSize('20').fontColor('green');
// We bind a listener function to the custom event
table.on('customHover', function(d, i){
    console.log('customHover: ' + d, i);
});

d3.select('body')
.datum(dataset)
.call(table);

First, we add our sub-namespace to the d3 namespace. It is not essential to do this, but name-spacing is good practice to not pollute the global space.

d3.edge = {};

We add the table module, which is a simple function returning a function. The outer function serves as the scoped closure for our module.

d3.edge.table = function module() {
    function exports() {
        //...
    }
    return exports;
};

Some variables are available in the closure and not accessible from the outside (private). They have default values.

d3.edge.table = function module() {
    var fontSize = 10,
        fontColor = 'red';

    function exports() {
        //...
    }
    return exports;
};

In JavaScript, a function is also an object, so we can attach some properties and methods to it. The functional aspect of JavaScript is very powerful, but it forces you to un-learn some habits from your OOP (Object Oriented Programming) background.

d3.edge.table = function module() {
    var fontSize = 10,
        fontColor = 'red';
    function exports(_selection) {
        //...
    }
    exports.fontSize = function(_x) {
        //...
    };
    exports.fontColor = function(_x) {
        //...
    };
    return exports;
};

These "public" functions will be used as getters and setters at the same time. They are getters when no argument is passed to the function; otherwise they set the private variable to the new value passed as an argument. When setting, we return the current context this, as we want these methods to be chainable.

exports.fontSize = function(_x) {
    if (!arguments.length) return fontSize;
    fontSize = _x;
    return this;
};

Let's now illustrate some less basic features. One way for the module to expose events to the outside world is by using an implementation of the pubsub (Publish/Subscribe) pattern. We use d3.dispatch to declare an event that we can then listen to from the outside when it's triggered in the module.

//Declare
var dispatch = d3.dispatch('customHover');

//Trigger
dispatch.customHover()
// Bind to
.on('customHover', function(){ /* user code ... */ });

For the event to be accessible from the outside, it needs to be bound to the module itself. We use d3.rebind for this task, rebinding the event to the "on" method (following D3.js convention).

d3.rebind(exports, dispatch, "on");

We now have a complete module. Now we need to build some basic HTML and attach it to our page. A D3.js selection will be passed to the function as the parent of our generated html. _selection.each loops through the selected elements and the _data object attached to each DOM element will be used as the text of our generated <div>. Some styling is using the private properties. That's basically the place where you put your standard D3.js code.

function exports(_selection) {
    _selection.each(function(_data) {
        d3.select(this)
        .append('div')
        .style({
            'font-size': fontSize + 'px',
            color: fontColor
        })
        .html('Hello World: ' + _data)
        .on('mouseover', dispatch.customHover);
    });
}

That's it for the module. The way to use it is to first instantiate the chart, then pass attributes and bind events.

var table = d3.edge.table().fontSize('20').fontColor('green');
table.on('customHover', function(d, i){
    console.log('customHover: ' + d, i);
});

The module will come into action once you pass your D3.js selection to be used as the parent DOM element.

d3.select('#figure')
.datum(dataset)
.call(table);

Using the D3.js selection.call function is equivalent do using it this way:

var parent = d3.select('#figure').datum(dataset);
table(parent);

That's it!  

Summary

We've shown the barebones of how to write a reusable D3.js plugin. In the next chapter we expand the complexity of the examples and show how to create multiple instances of the same plugin.

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

You should refresh this page.