Large d3.js Application Development

“Code that belongs”. This is the mantra, the quest, of Beezwax Senior Developer Ryan Simms; which he discusses in this ten-part article on building a large-scale web application using d3.js. How to write code that fits the context intrinsically. The article focuses on specific techniques with the data visualization library, d3.js. But the lessons are broad. How do you make something fit — in ways that make it feel like it belongs?

Introduction

The full name of our company is actually “Beezwax Datatools, Inc.” which is fitting because much of what we do is make tools to organize and analyze data. Often this includes data visualizations, which we generally use d3.js to build because of the versatility it provides.

While we initially built data visualizations fairly procedurally, it quickly became evident that we needed a more sustainable structure. Javascript is known for its lack of conventions compared to other languages, and the process of finding the “perfect” organizational structure led me down a path of existential self-discovery of what it means to write “code that belongs.” This path was also the basis for a talk I gave at 42 Silicon Valley titled, “Existential Dread Brought on by Coding Conventions, or Lack Thereof.” The “code that belongs” in this case was writing a d3 component that feels like a d3 component and not just some coding in my own style that happens to use the d3 library.

Code that belongs

Luckily we have an ideal example of how to organize components, and that is the d3.js library itself. In 2012, the author of d3, Mike Bostock wrote a blog article demoing a “strawman convention for reusable visualization components.” The format he suggested mirrored the way that many d3 components are organized. However, he left a very important part as a small note in the sidebar.

See the axis component and brush component for examples of chart components supporting interaction and animation automatically.

I’ve done my best to look to these components and extrapolate them to a format that will work for d3 applications of any size.

This article assumes some knowledge of d3.js. There are a ton of great beginner tutorials out there, and my favorites are the ones from Mike Bostock, like this walk-through making a bar chart.

Goal: Reduce coupling between chart components and delegate responsibility. Build components based on Mike Bostock’s example.

Component features:

  • A component is a closure with getter-setter methods.
  • Components should be as decoupled from each other as much as possible.
  • Use a mediator for communication between components rather than having them communicate directly.
  • Allow and embrace method chaining.
  • Set parameters using methods instead of passing a “parameters” argument.  These methods should return a value if not given a value (getter/setter).
  • Components should make no assumptions about which properties to use in the data. Instead, use accessor methods.
  • Dispatch custom events from each component using d3.dispatch.
  • Allow the binding of callbacks to component events using a public on method.
  • Use d3.local  to save component instance state between updates.
  • Allow components to be passed either a transition or selection.
  • Allow components to be passed multiple selections or a single selection.
  • Reuse the passed transition so everything stays in sync.
  • this by convention should always point to a DOM element.
  • Don’t update the DOM unless explicitly called.
  • Try to keep components slim.

I often find it easier to code “backwards”, in that I’ll start with how I want the implementation to look, and then build from there. Here’s how we want the API of the component to look. The idea is, it looks just like how you would interact with a component from the d3 library itself:

var barchart = barChartWithTransitions()
    .transitionDuration(400)
    .on('click', function(d) {console.log(d.name + ' clicked');});

d3.select('#chart1')
    .datum(someDataArray)
    .call(barchart.label, 'Quarterly Profits')
    .call(barchart);

Here’s how two components might interact, where clicking on an item on one component highlights the corresponding item on the other component:

var barchart = barChartWithTransitions()
    .transitionDuration(400)
    .on('click', function(dataOfClickedItem) {
      // this === #chart html node
      d3.select('#legend').highlight(dataOfClickedItem)
    });

var legend = basicLegend()
    .on('click', function(dataOfClickedItem) {
      // this === #legend html node
      d3.select('#chart').highlight(dataOfClickedItem)
    });
 
d3.select('#chart')
    .datum(someDataArray)
    .call(barchart);

d3.select('#legend')
    .datum(someDataArray)
    .call(legend)


You’ll notice that these two components are completely decoupled from each other. They don’t interact with each other directly but instead allow callbacks to be bound to events which they emit. If you were to remove one component, the other would continue working just fine.

Sometimes it’s inevitable that one component will have to call another directly, (i.e. – how a bar chart would call d3.axisBottom), but it’s worth considering how much to decouple components, and delegate communication to a mediator when it makes sense.

Here’s a template for a component with some sample content.

function barChartWithTransitions() {
  // outer scope available to public methods
  var events = d3.dispatch('click');
  var transitionDuration = 600; // default
  var label = d3.local();
  
  function chart(group) {
    // scope available to all contexts
    
    group.each(function(data) {
      // context-specific scope
      var context = d3.select(this),
          transition;
      
      // use transition if that's what was passed in, otherwise create a new one
      if (group instanceof d3.transition) {
        transition = context.transition(group);
      } else {
        transition = context.transition().duration(o.animationDuration);
      }
      
      /* render chart */
    })
  }
    
  // component-scope event listener binding
  chart.on = function(evt, callback) {
    events.on(evt, callback);
    return chart;
  };
  
  // component-scope getter/setter
  chart.transitionDuration = function(value) {
    if (!arguments.length) {return transitionDuration;}
    transitionDuration = value;
    return chart;
  };
  
  // context-specific getter/setter
  chart.label = function(context, value) {
    if (typeof value === 'undefined') {
      return context.nodes().map(function(node) {return label.get(node);});
    }
    context.each(function() {label.set(this, value);});
    return chart;
  };
  
  return chart;
}

Let’s make a small large scale d3.js application!

The following tutorial series will start with a simple donut chart. Then we’ll successively step through adding features while keeping concerns separated and components organized. While this example is not as large as some apps could be, it uses principles that will scale to much more intricate apps.

  1. Introduction
  2. From The Outside
  3. On the Inside
  4. Transitions
  5. Legend & Events
  6. Rotation & Selection
  7. Adding Description
  8. Icons
  9. Tests
  10. Refactoring & Conclusion

From The Outside

Introduction <– Previous Next –> On The Inside

This is part two of our ten part series for large d3.js application development. The series is intended to be followed sequentially, so be sure that you’ve read the previous section before proceeding.

Now that we’ve laid out the basics for a modular d3 component, let’s get to building an app using these techniques. Be sure that you’ve read the first section before continuing.

What we’re building

  • Donut Chart
  • Pie segments that animate when data is updated
  • Resizable with animation
  • Selected pie segment rotates to alignment angle
  • Icons are shown for each segment
  • Legend
  • Legend highlights when item is selected
  • Description is shown for selected segment
  • Description is not shown if selected segment is not contained in the data
  • Clicking on donut segment selects the legend item
  • Clicking on a legend item selects the donut segment
  • We’ll build two donuts to demonstrate how we can interact with them collectively or individually
  • If you’re impatient, you can see the final code here

Smith and Jones donut budgets

But first…

I’d first like to address a few things that this series will omit, not because they’re not great, but because I wanted to keep it focused:

We’ll start out by building a donut that:

  • Accepts data
  • Updates with new data
  • Has a label in the middle
  • Can be resized

Our HTML will be minimal, with only the sliders and empty divs for the donuts. The sliders will have a data-target attribute which will let us know which donut it should be resizing. I’ll be omitting CSS completely out of this tutorial.

Let’s assume that our data is simple and looks like this:

[
{id: 1, value: 8.56, color: 'red'}
// plus 4 more items
]

I’ve built data.js to generate the data and random value properties between 5-20.  It also can take a boolean splice argument which will return data of a random length 1-5.

API

Let’s start first by building app.js, so we can see how the API should look. Then we’ll build the API.

document.addEventListener('DOMContentLoaded', function() {
  var donut,
      events;

  function build() {
    // build donut component
  }

  function addToDom() {
    // call component on DOM elements
  }

  function addListeners() {
    d3.select('button').on('click', events.dataButtonClick);
    d3.selectAll('.donut-size').on('change', events.resizeSliderChange);
  }

  events = {
    dataButtonClick: function() {
      // generate new data and send to donuts
    },

    resizeSliderChange: function() {
      // resize stuff
    }
  };

  build();
  addToDom();
  addListeners();
});

Building Our Component

So how should the build() function look? Let’s first take a look at how d3’s line component is built.

var x = d3.scaleTime()
    .rangeRound([0, width]);

var y = d3.scaleLinear()
    .rangeRound([height, 0]);

var line = d3.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });

Notice that d3.line makes no assumptions as to how to handle the data. It has x and y methods which receive the data as an argument and return the x and y values. We’ll need to do something similar so that our component knows how to access the value, color, unique key, and sorting function. Let’s also allow for some customization by letting the user set the donut ring thickness.

donut = APP.rotatingDonut()
    .thickness(0.5)
    .value(function(d) {return d.value;})
    .color(function(d) {return d.color;})
    .key(function(d) {return d.id;})
    .sort(function(a, b) {return a.id - b.id;});

I’ll be using the APP namespace for all these components

Calling Our Component

Now how should we render this component on our DOM element? Let’s first take a look at how d3’s axis component does the same thing.

var xAxis = d3.axisBottom(d3.scaleTime().range([0, 600]))
    .tickSizeInner(4)
    .tickSizeOuter(8);

var group = d3.select('svg')
    .append('g')
    .attr('transform', 'translate(0,'  + height + ')')
    .call(xAxis);

Notice how the .call method is used here instead of passing the selection as a parameter like xAxis(group). The benefit to using selection.call is that it returns the selection and you can continue the method chaining.

Because we want each donut to have its own label, we’ll need to call selection.call  on each DOM element. We’ll be binding the data to each element rather than passing it some other way. This is the intended method by which data bindings work in d3, so why would we do anything else?

d3.select('#donut1')
    .datum(APP.generateData())
    .call(donut.label, 'Smith')
    .call(donut);

d3.select('#donut2')
    .datum(APP.generateData())
    .call(donut.label, 'Jones')
    .call(donut);

Events

Now we need to handle our two events. Clicking the Randomize Data button will simply bind new data to our elements and call donut on them to re-render.

d3.select('#donut1')
    .datum(APP.generateData(true))
    .call(donut);

d3.select('#donut2')
    .datum(APP.generateData(true))
    .call(donut);

For the slider resizing, we’ll want to pass the slider value to a method on our donut object. We’ll only want to resize the donut associated with the slider, so we’ll need to use selection.call  on the method just like we did with the label method.

var target = d3.select(this).attr('data-target'),
    value = this.value * 2; // this === slider element

d3.selectAll(target)
    .call(donut.dimensions, {width: value, height: value})
    .call(donut)
    .style('width', value + 'px')
    .style('height', value + 'px');

Final Code

So the final app.js file will look like:

document.addEventListener('DOMContentLoaded', function() {
  'use strict';
  var donut,
      events;

  function build() {
    donut = APP.rotatingDonut()
        .thickness(0.5)
        .value(function(d) {return d.value;})
        .color(function(d) {return d.color;})
        .key(function(d) {return d.id;})
        .sort(function(a, b) {return a.id - b.id;});
  }

  function addToDom() {
    d3.select('#donut1')
        .datum(APP.generateData())
        .call(donut.label, 'Smith')
        .call(donut);

    d3.select('#donut2')
        .datum(APP.generateData())
        .call(donut.label, 'Jones')
        .call(donut);
  }

  function addListeners() {
    d3.select('button').on('click', events.dataButtonClick);
    d3.selectAll('.donut-size').on('change', events.resizeSliderChange);
  }

  events = {
    dataButtonClick: function() {
      d3.select('#donut1')
          .datum(APP.generateData(true))
          .call(donut);

      d3.select('#donut2')
          .datum(APP.generateData(true))
          .call(donut);
    },

    resizeSliderChange: function() {
      var target = d3.select(this).attr('data-target'),
          value = this.value * 2;

      d3.selectAll(target)
          .call(donut.dimensions, {width: value, height: value})
          .call(donut)
          .style('width', value + 'px')
          .style('height', value + 'px');
    }
  };

  build();
  addToDom();
  addListeners();
});

Next –> On The Inside

On The Inside

From The Outside <– Previous  Next –> Transitions

This is part three of our ten part series for large d3.js application development. The series is intended to be followed sequentially, so be sure that you’re read the previous sections before proceeding.

Now that we have decided how the public API of our component should look, let’s build it. We already went over the basic frame in part 1, so we’ll flesh it out now.

First, we need to make sure that our APP namespace object exists. I’ll add this one line to the top of all the javascript files rather than trying to keep track of when and where it was initialized.

if(typeof APP === 'undefined') {APP = {};}

Structure

Next, we’ll define our component. Let’s review the basic structure and talk about the scope of each part.

APP.rotatingDonut = function() {
  // initialization-scope

  function donut(group) {
    // group-scope
    group.each(render);
  }

  function render(data) {
    // group-item-scope
    // rendering done here
  }
  
  donut.thickness = function(){/*...*/}
  // set other methods here

  return donut;
};

When we initialize our rotatingDonut component in app.js, the code in the initialization-scope runs. This consists of setting methods on the donut function and then returning it. At this point, we can call methods on the initialized method and chain them, because they all return the donut object. To review:

var myDonut = APP.rotatingDonut()
    .thickness(0.5)
    .value(function(d) {return d.value;})
    .color(function(d) {return d.color;})
    .key(function(d) {return d.id;})
    .sort(function(a, b) {return a.id - b.id;});

We can call myDonut.thickness or some other method to call that method, which in this case sets an option for the object. Or we can call myDonut itself to call the inner donut function. That function takes a d3 selection of one or more elements. Then for each item in the selection group, we call the render function, which does most of the work.

That inner donut function is generally called like this:

d3.select('#donut1').call(myDonut);

…which is functionally the same as this method except the former allows chaining.

myDonut(d3.select('#donut1'));

Initialization-scope

I generally store publicly-settable values in an options object, which I abbreviate to o because we’ll be using it all over. I also have a similar object for things that need to be attached to a group-item and persist between renders. We’ll cover that more in a bit.

var o = {
  thickness: 0.4,
  value: null,
  color: null,
  key: null,
  sort: null
};

var local = {
  label: d3.local(),
  dimensions: d3.local()
};

We can make getter/setter methods for these options like so:

donut.thickness = function(value) {
  if (!arguments.length) {return o.thickness;}
  o.thickness = value;
  return donut;
};

If an argument is passed, then it sets the equivalent property on the options object and returns donut to allow chaining. If not, then it returns that value. These values belong to the myDonut object we created in app.js and will always be the same for every instance of our component.

We have two items, label and dimensions, which should be specific to each instance of the donut. For this we’ll use d3.local() which was introduced in v4 as a better way to bind to an HTML node. If you’re not familiar with d3.local, take a second to read about them before proceeding.

Remember that these methods will be called from app.js like this:

d3.select('#donut1')
    .call(myDonut.label, 'Smith')

The getter/setter can be defined like this:

donut.label = function(context, value) {
  var returnArray;
  if (typeof value === 'undefined' ) {
    returnArray = context.nodes()
        .map(function (node) {return local.label.get(node);});
    return context._groups[0] instanceof NodeList ? returnArray : returnArray[0];
  }
  context.each(function() {local.label.set(this, value);});
  return donut;
};

This is basically the same as the simpler getter/setter above, but because the context argument can be a selection for multiple items, we have to loop through them and return a map of the results. However, if I pass a single element using d3.select instead of d3.selectAll, I would expect to be returned a single result instead of an array. That’s why we check context._groups[0] instance NodeList before returning an array or the contents of an array.

Group-scope

Generally not a whole lot goes in this scope because you’ll normally want your logic to act at the group-item-scope level, but you may want to check the state of some things here or set group-level values.

Group-item-scope

This is where you’ll see more familiar chart-building code. The data bound to the selection is passed as an argument and this references the HTML node of the selection. I’m not going to walk through most of this code because it’s pretty standard d3 code, but I’ll highlight a few items of interest.

Even though the data is already bound to the selection, we’ll want to convert it to pie data and rebind it:

var context = d3.select(this)

var pie = d3.pie()
    .value(o.value)
    .sort(null);

context.selectAll('svg')
    .data([pie(data.sort(o.sort))])
    .enter()
    .append('svg')
    .append('g')
    .attr('class', 'group')
    .append('text')
    .attr('class', 'donut-label')

This format uses d3’s general update pattern to append a single SVG element if one doesn’t already exist, otherwise it will reuse the existing one. Also, note that we’re using the functions that were passed in from our publicly accessible methods to sort the data and tell the pie component how to find the value in the data.

Here’s where we’re using the local value for which we built that getter/setter earlier

context.select('text.donut-label')
    .text(local.label.get(context.node()));

There are a couple of things going on here:

  1. We need to tell d3 how to access the nested data. Object is just an equivalent to function(d) {return d;} when the argument is an object. You can also use String or Number if you are sure of the format of the nested data.
  2. We’re supplying a key function so the d3 will know what data is persistent between data updates. For this I’ve created a little helper function which uses the passed-in function to find the key in the data.
segments = context.selectAll('svg')
    .select('g.group')
    .selectAll('path.segment')
    .data(Object, dataAccess('key'));


function dataAccess(key) {
  return function(d) {
    return o[key](d.data);
  };
}

Final Code

That’s about it. There are a few things I’ve skipped, but the full code is here:

if(typeof APP === 'undefined') {APP = {};}
APP.rotatingDonut = function() {
  'use strict';
  var o,
      local;

  o = {
    thickness: 0.4,
    value: null,
    color: null,
    key: null,
    sort: null
  };

  local = {
    label: d3.local(),
    dimensions: d3.local()
  };

  function donut(group) {
    group.each(render);
  }

  function render(data) {
    var context,
        dim,
        pie,
        arc,
        segments,
        segmentEnter;

    if (!data) {return;}

    context = d3.select(this);
    dim = getDimensions(context);

    pie = d3.pie()
        .value(o.value)
        .sort(null);

    arc = d3.arc()
        .outerRadius(dim.outerRadius)
        .innerRadius(dim.innerRadius);

    context.selectAll('svg')
        .data([pie(data.sort(o.sort))])
        .enter()
        .append('svg')
        .append('g')
        .attr('class', 'group')
        .append('text')
        .attr('class', 'donut-label')
        .attr('text-anchor', 'middle')
        .attr('dominant-baseline', 'middle');

    context.selectAll('svg')
        .attr('width', dim.width)
        .attr('height', dim.height)
        .selectAll('g.group')
        .attr('transform', 'translate(' + dim.width / 2 + ',' + dim.height / 2 + ')');

    context.select('text.donut-label')
        .text(local.label.get(context.node()));

    segments = context.selectAll('svg')
        .select('g.group')
        .selectAll('path.segment')
        .data(Object, dataAccess('key'));

    segmentEnter = segments.enter()
        .append('path')
        .attr('class', 'segment')
        .attr('fill', dataAccess('color'));

    segmentEnter
        .merge(segments)
        .attr('d', arc);

    segments.exit()
        .remove();
  }

  function dataAccess(key) {
    return function(d) {
      return o[key](d.data);
    };
  }

  function getDimensions(context) {
    var thisDimensions = local.dimensions.get(context.node()) || {},
        width = thisDimensions.width || context.node().getBoundingClientRect().width,
        height = thisDimensions.height || context.node().getBoundingClientRect().height,
        outerRadius = Math.min(width, height) / 2,
        innerRadius = outerRadius * (1 - o.thickness);

    return {
      width: width,
      height: height,
      outerRadius: outerRadius,
      innerRadius: innerRadius
    };
  }

  donut.thickness = function(_) {
    if (!arguments.length) {return o.thickness;}
    o.thickness = _;
    return donut;
  };
  donut.value = function(_) {
    if (!arguments.length) {return o.value;}
    o.value = _;
    return donut;
  };
  donut.color = function(_) {
    if (!arguments.length) {return o.color;}
    o.color = _;
    return donut;
  };
  donut.key = function(_) {
    if (!arguments.length) {return o.key;}
    o.key = _;
    return donut;
  };
  donut.sort = function(_) {
    if (!arguments.length) {return o.sort;}
    o.sort = _;
    return donut;
  };

  donut.dimensions = function(context, _) {
    var returnArray;
    if (typeof _ === 'undefined' ) {
      returnArray = context.nodes()
          .map(function (node) {return local.dimensions.get(node);});
      return context._groups[0] instanceof NodeList ? returnArray : returnArray[0];
    }
    context.each(function() {local.dimensions.set(this, _);});
    return donut;
  };
  donut.label = function(context, _) {
    var returnArray;
    if (typeof _ === 'undefined' ) {
      returnArray = context.nodes()
          .map(function (node) {return local.label.get(node);});
      return context._groups[0] instanceof NodeList ? returnArray : returnArray[0];
    }
    context.each(function() {local.label.set(this, _);});
    return donut;
  };

  return donut;
};

Complete Code

You can preview the result by hitting the play button in the header of the embed below. You can also click on the external link icon to open the plunker in a new window.

Next –> Transitions

Transitions

On The Inside <– Previous  Next –> Legend & Events

This is part four of our ten part series for large d3.js application development. The series is intended to be followed sequentially, so be sure that you’re read the previous sections before proceeding.

Adding a Feature

We have a great base for our application, and as it grows, we’ll make a point to try to keep the rotatingDonut component from getting bloated with additional features. Instead, we’ll compartmentalize new features into additional components where we can.

Currently, when new data is bound and the render called, the chart updates immediately without any transitions. Transitioning the state of a pie/donut is actually a little tricky because it requires setting entering angles, exit angles, and transition angles for all segments, as well as keeping track of previous data so we can calculate these angles. Here’s a list of what we’ll need to accomplish here:

  • Resizing a donut using the slider should be transitioned
  • All segments need an attribute tween for the path’s 'd' attribute
  • The attribute tween will need to interpolate startAngle, endAngle, innerRadius, and outerRadius
  • We’ll need to save a segment’s previous data so we can use it in the interpolation
  • Entering segments will start as an infinitesimal slice and transition to their final state
  • Exiting segments will transition to an infinitesimal slice.

The last two are tricky because we need to find out where that slice should begin to grow and shrink.

Let’s try to make the minimal amount of changes to our rotatingDonut() component

  1. Expose an animationDuration method to set the default duration time
  2. If the component is called on a transition rather than a selection, then reuse that transition
  3. Create a pieTransition() component, and set the arc and sort methods
  4. Call pieTransition().enteringSegments and pieTransition().transitioningSegments methods and pass the corresponding selections.  This is necessary for pieTransition to be able to figure out the start and end angles for each segment.
  5. Call corresponding pieTransition methods on the entering, exiting, and transitioning segments to transition these segments.

Reusing the Transition

For #2, we’ll check if the passed-in selection is an instance of d3.transition and if so, reuse it in the context’s transition. Otherwise, we’ll create a new transition using the animationDuration option.

 if (group instanceof d3.transition) {
  t = d3.transition(group);
} else {
  t = d3.transition().duration(o.animationDuration);
}

We’ll then use this t variable whenever we call a transition.  This will make sure that all of our transitions stay in sync.

context.selectAll('svg')
    .transition(t)
    .attr('width', dim.width)
    .attr('height', dim.height)

Calling the Component

Because the entering, exiting, and transitioning segments all need to be treated a little bit differently, we’ll call different pieTransition methods on them:

segmentEnter
    .transition(t)
    .call(pieTransition.enter);

segments
    .transition(t)
    .call(pieTransition.transition);

segments.exit()
    .transition(t)
    .call(pieTransition.exit)
    .remove();

The pieTransition object is set up similar to how our rotatingDonut component was set up. We’re using d3.local to store the instance of pieTransition because the pieTransition component is keeping track of previous donut data and so it needs to be persistent between renders. Also we need to have a separate instance of pieTransition for each donut.

local = {
  label: d3.local(),
  animate: d3.local(),
  dimensions: d3.local()
};

pieTransition = local.animate.get(this) || local.animate.set(this, APP.pieTransition());

pieTransition
    .arc(arc)
    .sort(o.sort)
    .enteringSegments(segmentEnter)
    .transitioningSegments(segments);

One thing to note here is that pieTransition needs the entering and transitioning segments to figure out how to calculate the angles for all the segments. That code is a little complicated and out of the scope of this post, but you can review it by viewing the diff at the end of this section if you’d like.

Component Code

Here’s the structure of the new component,

APP.pieTransition = function() {
  var enteringSegments,
      transitioningSegments;

  var previousSegmentData = d3.local();

  var o = {
    arc: null,
    sort: null
  };

  var methods = {
    enter: function(transition) {
      transition
          .each(setEnterAngle)
          .call(render);
    },
    transition: render,
    exit: function(transition) {
      transition
          .each(setExitAngle)
          .call(render);
    }
  };

  function previousAdjacentAngle() {
    // returns angle of adjacent segment using previous data
  }

  function currentAdjacentAngle() {
    // returns angle of adjacent segment using current data
  }

  function updateNodes() {
    // update variables which need to be correct 
    // when previousAdjacentAngle or  currentAdjacentAngle is called
  }

  function setEnterAngle() {
    // set enter angle of the segments and 
    // bind the node's current data to the node using previousSegmentData
  }

  function setExitAngle(d) {
    // set exit angle of the segments
  }

  function render(transition) {
    transition.attrTween('d', arcTween);
  }

  function arcTween() {
    // return tweening function using interpolate()
  }

  function interpolate() {
    // return d3.interpolate between old data and new
  }

  methods.enteringSegments = function (_) {
    enteringSegments = _;
    updateNodes();
    return methods;
  };

  methods.transitioningSegments = function (_) {
    transitioningSegments = _;
    updateNodes();
    return methods;
  };

  methods.arc = // getter/setter;
  methods.sort = // getter/setter};

  return methods;
};

Complete Code

I would highly recommend reviewing the full diff here so you can clearly see how we added this new functionality.

You can preview the result by hitting the play button in the header of the embed below. You can also click on the external link icon to open the plunker in a new window.

Next –> Legend & Events

Legend & Events

Transitions <– Previous  Next –> Rotation & Selection

This is part five of our ten part series for large d3.js application development. The series is intended to be followed sequentially, so be sure that you’re read the previous sections before proceeding.

Adding a Legend

In the last section, we kept our rotatingDonut component slim by adding the new pieTransition functionality to a separate component. In this section, we’ll add a legend which will be completely decoupled from our donut.

The desired behavior for this component is that when we hover over a donut segment, it will highlight the corresponding item in the legend. Our rotatingDonut component knows nothing of our new basicLegend component, and we want to keep it that way. It’s the job of the mediator (in our case app.js) to facilitate communication between the two. To get this to work, any component that needs to have externally-accessible events should have an on method to allow binding of callbacks to these events. Let’s look at how d3-brush handles events.

var brush = d3.brushX()
    .extent([[0, 0], [width, height]]);

brush.on("brush end", function() {
  // get brush selection information and zoom the svg or whatever
});

This is a very familiar event callback pattern for javascript libraries, and we’ll build ours the same way.

The API

First, let’s look at how we want the API of our legend to look. We’ll want a method to highlight a legend item when we hover over a donut segment, and we’ll eventually want to do something when we click on a legend item.

// build component
var myLegend = APP.basicLegend();

// add to dom
d3.select('#legend')
    .datum(someData)
    .call(myLegend);

// highlight item
myDonut.on('mouseenter', function(d) {
  d3.select('#legend').call(myLegend.highlight, d)
});

// unhighlight item
myDonut.on('mouseleave', function(d) {
 d3.select('#legend').call(myLegend.unhighlight, d)
});

// legend event listener (not yet used)
myLegend.on('click', someCallback);

You can see how this completely decouples the events and callbacks between the two components. Each component doesn’t care what happens when it emits an event, and it doesn’t care how its methods are called.

Adding an Event Listener to rotatingDonut

rotatingDonut is missing a method to add a callback to a click event, so let’s add that first. We’ll use d3.dispatch to create an object that will handle our events.

var events = d3.dispatch('mouseenter', 'mouseleave')

Then we’ll create an on method which allows binding callbacks to this object.

donut.on = function(evt, callback) {
  events.on(evt, callback);
  return donut;
};

Finally, we want to dispatch this event when a segment is clicked. d3.dispatch allows this of the callback to be set, so we’ll use the HTML node of the donut(not the segment). We’ll also pass the data of the segment.

segments.enter()
    .append('path')
    .attr('class', 'segment')
    .attr('fill', dataAccess('color'))
    .on('mouseenter mouseleave', function(d) {
      events.call(d3.event.type, context.node(), d.data);
    });

We currently don’t have any way of highlighting a donut segment (we’ll get to that in the rotation section) so we don’t need to add a listener to the legend yet. However, I’m actually reusing this legend from another project so it may have some unused features. I suppose the real goal of building reusable components is to eventually reuse them.

Creating the Legend

The code for the legend is very similar to the rotatingDonut. Much of the code here has been replaced with comments so we can better focus on events and communication between the components.

APP.basicLegend = function () {
  var events = d3.dispatch('mouseenter', 'mouseleave', 'click'),
      selectedItem = d3.local();

  // set options

  function legend(group) {
    group.each(render);
  }

  function render(data) {
    // render legend

    labels.enter()
        .attr('data-id', o.key)
        .on('mouseenter mouseleave', function() {
          // call highlight
          // emit event
        })
        .on('click', function(d) {
          // set selected item
          // re-render legend
          // emit event
        })

    /* add or remove 'selected' class to element 
       which matches the selectedItem d3.local variable */
  }

  function highlight(selection, d, action) {
    selection
        .selectAll('li[data-id="' + o.key(d) + '"]')
        .classed('hovered', action === 'mouseenter');
  }

  // add getter/setter methods

  legend.on = function(evt, callback) {
    events.on(evt, callback);
    return legend;
  };

  legend.selectedItem = // context-specific getter/setter

  legend.highlight = function(selection, d) {
    selection.call(highlight, d, 'mouseenter');
    return legend;
  };

  legend.unhighlight = function(selection, d) {
    selection.call(highlight, d, 'mouseleave');
    return legend;
  };

  return legend;
};

This legend already has actions attached to mouseenter, mouseleave, and click. Notice that the actions associated with these events can be called both internally (highlightselectedItem.set) and externally (legend.highlight, legend.unhighlight, legend.selectedItem). We also have an on method just like we added to the rotatingDonut component.

Method Styles

I’d like to point out the difference in implementation in the highlight/unhighlight methods and selectedItem method.

selectedItem – sets a state using a d3.local and the legend must be re-rendered for the DOM to be updated.

// app.js
d3.select('#legend')
    .call(myLegend.selectedItem, d)
    .call(myLegend);

// basic_legend.js
var selelectedItem = d3.local();

legend = function() {
  // build legend
  labelsEnter
      .merge(labels)
      .classed('selected', isSelected)
}

function isSelected(d) {
  return o.key(d) === o.key(selectedItem.get(this));
}

legend.selectedItem = // context-specific getter/setter for selectedItem variable;

highlight/unhighlight – finds the matching item by data attribute and toggles a ‘hovered’ class.

// app.js
d3.select('#legend')
    .call(legend.highlight, d)

// basic_legend.js
legend.highlight = function(selection, d) {
  selection.call(highlight, d, 'mouseenter');
  return legend;
};

function highlight(selection, d, action) {
  selection
      .selectAll('li[data-id="' + o.key(d) + '"]')
      .classed('hovered', action === 'mouseenter');
}

Even though selection and highlighting are fairly similar actions, there are a few reasons for the different approaches:

  1. selectedItem requires a persistent state, and it’s best to keep that state explicit rather than relying on a CSS class or on a DOM object. It also doesn’t rely on using DOM attributes to find a matching item, which is good.
  2. highlight/unhighlight may be called rapidly as the user moves the cursor around the donut. We need to update highlighting as efficiently as possible, and re-rendering the entire legend can be computationally expensive compared to finding and adjusting the matching element.

To me, the first approach feels more like d3 and so I prefer to use that method unless performance needs to be considered.

Complete Code

I would highly recommend reviewing the full diff here so you can clearly see how we added this new functionality.

You can preview the result by hitting the play button in the header of the embed below. You can also click on the external link icon to open the plunker in a new window.

Next –> Rotation & Selection

Rotation & Selection

Adding a Legend and Events <– Previous  Next –> Adding Descriptions

This is part six of our ten part series for large d3.js application development. The series is intended to be followed sequentially, so be sure that you’re read the previous sections before proceeding.

Introduction

In this section, we’ll be adding the rotation feature to our rotating donut. The idea is that you can specify an alignment angle, and then when a segment is selected, the donut will rotate to that alignment angle. This seems like a pretty straightforward feature, but there’s actually a fair amount that needs to be done to implement it.

  • Ability to select a donut segment
  • The state of the selection needs to be persistent
  • Specify an alignment angle (i.e. 90 degrees)
  • Pass the alignment angle to our pieTransitions component
  • Calculate the new offset angle for a segment when it is selected
  • The donut should not rotate more than 180 degrees to get to its new angle
  • If the selected data is not present in a donut, then the donut should not do anything
  • If the selected data becomes available in a donut, then it should rotate to that segment

Also, we want to change our rotatingDonut component as little as possible. Because the selection and rotation features are so tightly related, we’ll put them both in one component.

Additionally, we’ll want to add some more events to our donut and legend so that interacting with one causes an action on the other.

The Outside

Once again, let’s take the outside-in approach to adding this functionality, so let’s start with the changes we’ll make to app.js.

myDonut = APP.rotatingDonut();
myLegend = APP.basicLegend();
    
myDonut.alignmentAngle(90);

myDonut.on('click', function(d) {
  
  // select segment on other donuts
  var container = this;
  d3.selectAll('.donut')
      .filter(function() {return this !== container;})
      .call(myDonut.selectedSegment, d)
      .call(myDonut);

  // select segment on legend
  d3.select('#legend')
      .call(legend.selectedItem, d)
      .call(legend);
});

myLegend.on('click', function() {
  // select segment on all donuts
  d3.selectAll('.donut')
      .call(myDonut.selectedSegment, d)
      .call(myDonut);
});

All we’re doing here is setting the alignment angle of our donut and then calling the selectedItem and selectedSegment methods on the donut and legend when the other one is clicked. For our purposes, alignmentAngle is set at the component level, so every instance of the donut will have the same alignment angle. If we needed to be able to set the angle on a per-instance basis, then we’d build it the same way that we built the label or dimensions methods.

Pass Through Methods

Our selection and alignment angle will be maintained in our new component, but we’ll need to expose getter/setter methods for it on our rotatingDonut component so that outside elements (legend, app.js) can call these methods.

donut.selectedSegment = function(context, d) {
  if (typeof d === 'undefined' ) {return rotation.selectedSegment(context.select('svg'));}
  rotation.selectedSegment(context.select('svg'), d);
  return donut;
};

donut.alignmentAngle = function(_) {
 if (typeof _ === 'undefined' ) {return rotation.alignmentAngle();}
 rotation.alignmentAngle(_);
 return donut;
 };

Additional Click Event

We’ll add a ‘click’ event to our rotating donut similar to the existing events. The difference here is that on the click event, rotation.selectedSegment is called and the donut is re-rendered before the event is emitted.

var events = d3.dispatch('mouseenter', 'mouseleave', 'click');

var rotation = APP.pieSelectionRotation();

function render() {
  // existing code omitted...
  var context = d3.select(this) // node

  segments.enter().on('mouseenter mouseleave click', function(d) {
    if (d3.event.type === 'click') {
      rotation.selectedSegment(context.select('svg'), d.data);
      context.call(donut);
    }
    events.call(d3.event.type, context.node(), d.data);
  });
}

Using the Offset

Let’s look at how we’ll use the offset before we figure out how to calculate it. We need to find where the segment start and end angles are being calculated and add the offset there. Luckily, we’ve kept our code clean, so we only need to add the offset in one place.

function interpolate(segment) {
  var d = d3.select(segment).datum();
  var newData = {
    startAngle: d.startAngle + offset, // add offset here
    endAngle: d.endAngle + offset, // and here
    innerRadius: o.arc.innerRadius()(),
    outerRadius: o.arc.outerRadius()()
  };
  return d3.interpolate(previousSegmentData.get(segment), newData);
}

To get that offset value to the pieTransition, we’ll add a method and call it from rotatingDonut

pieTransition
    .arc(arc)
    .sort(o.sort)
    .enteringSegments(segmentEnter)
    .transitioningSegments(segments)
    .offset(rotation.getAngle(context.select('svg')));

The Hard Part

Our pieSelectionRotation component is now being passed the data for the selected segment and the alignment angle. Now we have to figure out how the getAngle method will return the correct angle. This component is a little different in that it’s not rendering anything, but rather is analyzing the data and returning the offset angle. However, I’ll keep the basic form for the component that we’ve been using. While reading through this, keep in mind the order in which this is processed:

  1. selectedSegment is called, which sets the selectedKey d3.local variable to the key of the data which is passed. In our case, the key is the id property.
  2. rotation is called on the selection.
  3. rotation looks for data which matches the selectedKey which was set in step 1.
  4. rotation sets the selected segment to the matching data.
  5. If matching data is found, then it sets the angle d3.local variable which is bound to the selection.
  6. Finally, getAngle will be called which returns the angle set in step 5.

You may have noticed above that we were passing context.select('svg') instead of just context.  This is because the SVG element is the node to which the main data was bound. We’ll need to access that data here and pull the piece which matches the selectedKey. With that in mind, we need to call rotation(step 2 above) on the selection to which our data was bound.

context.selectAll('svg')
    .data([pie(data.sort(o.sort))])
    .call(rotation)

Here’s the abbreviated code for the new component:

APP.pieSelectionRotation = function() {
  var local = {
    angle: d3.local(),
    selectedSegment: d3.local(),
    selectedKey: d3.local()
  };

  var o = {
    key: null,
    alignmentAngle: 0
  };

  function rotation(group) {
    group.each(function() {
      var selectedData = getSelectedData(this);

      local.angle.set(this, local.angle.get(this) || 0);
      local.selectedSegment.set(this, selectedData);

      if (selectedData) {
        local.angle.set(this, newAngle(local.angle.get(this), meanAngle(selectedData)));
      }
    });
  }

  function newAngle(offsetAngle, currentAngle) {
    // returns the new offset angle using degreesToRadians and shorterRotation
  }

  function meanAngle(data) {
    // returns mean of data.startAngle and data.endAngle
  }

  function degreesToRadians(degrees) {
    // converts angle value from degrees to radians
  }

  function shorterRotation(offset) {
    // return the angle with the shortest rotation so the donut doesn't spin more than it needs to
  }

  function getSelectedData(node) {
    // returns the data for the selectedKey for the passed node
  }

  rotation.selectedSegment = /** context-specific getter/setter
  * The difference there is that it sets selectedKey, but gets selectedSegment
  **/


  rotation.getAngle = // gets the d3.local "angle" value for the passed node(s)

  rotation.key = // getter/setter
  rotation.alignmentAngle = // getter/setter

  return rotation;
};

It’s worth noting that this component went through many various complicated versions before arriving at this relatively simple version.

Complete Code

I would highly recommend reviewing the full diff here so you can clearly see how we added this new functionality.

You can preview the result by hitting the play button in the header of the embed below. You can also click on the external link icon to open the plunker in a new window.

Next –> Adding Description

Adding Description

Rotation & Selection <– Previous Next –> Icons

This is part seven of our ten part series for large d3.js application development. The series is intended to be followed sequentially, so be sure that you’re read the previous sections before proceeding.

Introduction

We’re on a roll now with adding new features in an organized manner that hopefully feels at home with d3.js. In this section, we’ll be adding a third separate component to our app, a description of the selected segment with an arrow that points to the selection. This will give the rotation that we added in the last section a clearer purpose.

  • When an item is selected, a label and description for that item is shown.
  • If a donut does not contain data for a selected item, then a ‘no-data’ class is added so the text and arrow can be styled differently.

That’s it. This will be fairly quick to implement with our current pattern.

The Outside

description = APP.descriptionWithArrow()
    .label(function(d) {return formatDollar((d || {}).value);})
    .text(function(d) {return d ? d.description : 'no data for selection';});

function formatDollar(num) {
  return typeof num === 'number' ? '$' + num.toFixed(2) : '';
}

function setDescriptions() {
  d3.select('#description1')
      .datum(myDonut.selectedSegment(d3.select('#donut1')))
      .call(description);

  d3.select('#description2')
      .datum(myDonut.selectedSegment(d3.select('#donut2')))
      .call(description);
}

We’re continuing with the idea that the component should make few assumptions about the application outside of it, and so we’re going to have methods which explicitly state how to display the label and description.

Accessor functions are supplied to the label and text methods to tell the component exactly how to deal with our data.

The setDescription function can be called whenever we want to update the data(for example, when a donut or legend is clicked and a selection is made). By binding the data to the DOM selection and then calling the component on that selection, we are using d3’s intended method of passing data to a component.

The Inside

Now that you’ve gone through building a few of these components, this might be a good time to test yourself and see if you can build the component to match the API calls above. I’ll supply the part of the code that actually builds the DOM elements, minus a few things. See if you can build a descriptionWithArrow component around this.

function render(data) {
  var context = /* ??? */,
      right;

  context
      .html('')
      .classed('no-data', /* ??? */)
      .append('div')
      .attr('class', 'desc-left arrow')
      .html('&larr;');

  right = context.append('div')
      .attr('class', 'desc-right');

  right.append('div')
      .attr('class', 'label')
      .text(/* ??? */);

  right.append('div')
      .attr('class', 'text')
      .text(/* ??? */);
}

Complete Code

I would highly recommend reviewing the full diff here so you can clearly see how we added this new functionality.

You can view the completed code here. Click on description_with_arrow.js on the file list in the left panel to see the component as I wrote it. You may want to open the plunker in a new window so it’s not so cramped.

Next –> Icons

Icons

Adding Description <– Previous Next –> Tests

This is part eight of our ten part series for large d3.js application development. The series is intended to be followed sequentially, so be sure that you’re read the previous sections before proceeding.

Introduction

In this section, we’ll be adding an icon to each donut section.

  • Icons will be an external image, in this case, an SVG
  • Icons should stay in the center of each donut section, even during transitions
  • Icons attached to entering and exiting segments should fade in and out, and completely removed upon exit

Continuing with the idea of keeping components decoupled, we’ll try to alter our rotatingDonut component as little as possible, and keep new functionality within this new component as much as possible. This principle will also help guide our coding choices, as otherwise some choices may be unclear.

The Outside

In building this component, it was not clear how the API should look, and there was a fair amount of refactoring to get it to look as clean as possible. I settled on the following flow:

  1. pieIcons component is created
  2. pieIcons is called on entering donut segments and icons are appended to the container of the donut segments
  3. Icons and their radii are bound to the donut segments.
  4. pieIcons is called on transitioning segments and icons tween their transform: translate CSS style to their new location
  5. Exiting icons are faded out and removed from the DOM

And here’s how the API looks from the rotatingDonut component:

var pieIcons = APP.pieIcons()
    .container(/* function which returns the container of the donut */)
    .iconPath(/* accessor function for the icon's path, which will be passed in from app.js */)
    .imageWidth(/* outerRadius * thickness * iconSize */);

And here’s how it’s used:

segments.enter()
    .call(pieIcons)
    .transition()
    .call(pieIcons.tween);

segments
    .transition()
    .call(pieIcons.tween);

segments.exit()
    .transition()
    .call(pieIcons.exitTween);

Reusing Our Interpolation

Whenever a transition occurs, we’ll apply an attrTween to the transform attribute of each icon. The formatting of this attrTween uses an interpolation between the old angles and radius and the new angles and radius. In fact, we’ve already written this interpolation when we wrote the pieTransition component.

function interpolate(segment) {
  var d = d3.select(segment).datum();
  var newData = {
    startAngle: d.startAngle + o.offset,
    endAngle: d.endAngle + o.offset,
    innerRadius: o.arc.innerRadius()(),
    outerRadius: o.arc.outerRadius()()
  };
  return d3.interpolate(previousSegmentData.get(segment), newData);
}

Let’s use this same interpolation for ours. We could copy and paste, but then we’d die a little inside. Instead, let’s move this function from a private function to a public method which we can access through the API. We can then pass it to our pieIcons component like so:

var pieIcons = APP.pieIcons(); //...

pieIcons.interpolate(pieTransition.interpolate)

The Inside

A few notes:

  • The tween and exitTween methods are being passed a selection of donut segments. We need to fetch the associated icon from the supplied segment, so we’re using d3.local to store and retrieve those associations.
  • We’re reusing the transition passed into the tween and exitTween methods. This should ensure that the icons stay in sync with their associated donut segments.
  • A container method helps us to figure out where to append the icons. If one is not provided, we use the default on line 8. This default would not be ideal however, in that it uses an undocumented property of the selection.
  • We’re using d3.arc().centroid to find the coordinates of the center of our segments. I realized that I had rewritten the functionality of this function almost exactly before realizing that I could just use the built-in method. Lesson learned.
  • We’re removing the icons after the transition is over if the associated segment is gone. We’re using transition.on('end', callback) for this because we have to wait until the transition is over before we know if the segment is gone or not.
APP.pieIcons = function() {
  var icon = d3.local();

  var o = {
    iconPath: null,
    imageWidth: null,
    interpolate: null,
    container: function (selection) {return d3.select(selection._parents[0]);}
  };

  function icons(group) {
    var container = o.container(group);

    group.each(function(data) {
      render.call(this, data, container);
    });
  }

  function render(data, container) {
    var thisIcon = container
      .append('image')
      .attr('class', 'icon')
      .attr('xlink:href', o.iconPath.bind(null, data))
      .attr('width', o.imageWidth)
      .attr('height', o.imageWidth)
      .style('opacity', 0);

    icon.set(this, thisIcon);
  }

  function iconTranslate(i, t) {
    var dimensions = this.getBoundingClientRect(),
        coords = d3.arc().centroid(i(t)),
        adjustedCoords = [
          coords[0] - dimensions.width / 2,
          coords[1] - dimensions.height / 2
        ];

    return 'translate(' + adjustedCoords.join(',') + ')';
  }

  function removeIfParentIsGone(pieSegment) {
    return function() {
      if (!document.body.contains(pieSegment)) {
        this.remove();
      }
    };
  }

  function iconTween(pieSegment) {
    var i = o.interpolate(pieSegment);
    return function () {
      return iconTranslate.bind(this, i);
    };
  }

  icons.tween = function (transition, isExiting) {
    transition.selection().each(function () {
      icon.get(this)
          .transition(transition)
          .duration(transition.duration())
          .attr('width', o.imageWidth)
          .attr('height', o.imageWidth)
          .style('opacity', Number(!isExiting))
          .attrTween('transform', iconTween(this))
          .on('end', removeIfParentIsGone(this));
    });
  };

  icons.exitTween = function(transition) {
    icons.tween(transition, true);
  };

  // getter/setters for iconPath, imageWidth, interpolate, container

  return icons;
};

Complete Code

I would highly recommend reviewing the full diff here so you can clearly see how we added this new functionality.

You can preview the result by hitting the play button in the header of the embed below. You can also click on the external link icon to open the plunker in a new window.

Next –> Tests

Tests

Adding Icons <– Previous  Next –> Refactoring & Conclusion

This is part seven of our ten part series for large d3.js application development. The series is intended to be followed sequentially, so be sure that you’re read the previous sections before proceeding.

Introduction

Writing tests for d3 charts can be awkward if the chart is written monolithically. Luckily for us, we’ve separated out concerns so we can test each component individually. Testing will ensure that we don’t break existing functionality with a future update. Additionally, going through the test-writing process itself will often reveal some issues that you might not have otherwise discovered. This section will be less about specific tests and how to use the testing library, but instead will be about the broader process of setting up a testing workflow that makes sense for d3.

Finding Tools That Work

In deciding which testing library to use, the obvious choice was to use the same one used to test d3.js. Mike Bostock uses tape for testing and recommends using the same for convention. Tape seems to run best in a node.js environment, although it can be coerced to run in a browser. Node.js doesn’t have a window object, so much of our code would not work without some changes or mocking up a window object using jsdom. Jsdom is a “pure-JavaScript implementation of many web standards, notably the WHATWG DOM and HTML Standards, for use with Node.js.” This generally works as expected, but problems arise when trying to use SVG with jsdom, as SVG support has problems.  It looks like folks are working on this, but I ran into some insurmountable challenges when writing tests. One alternative is to use browserify to compile your node tests to code that will run in the browser, then use tape-run or similar to run the compiled code in a headless browser.  I found this workaround clunky, slow, and a bummer to use.

My solution was not to use tape at all but instead, use mocha.js .  Mocha was designed to run in the browser, so it should have more predictable results.  Furthermore, you can use mocha with phantomjs to run tests from the terminal or on continuous integration.

Mocha

With mocha, you’ll also need to choose an assertion library, which does the actual testing. Mocha runs the tests that are generated using the assertion library that we chose here, chai.

We’ll be running mocha in the browser, so we’ll need to first create a test-runner HTML file.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Mocha Tests</title>
  <script src="../node_modules/d3/build/d3.js"></script>
  <script src="../js/data.js"></script>
  <script src="../js/rotating_donut.js"></script>
  <script src="../js/pie_selection_rotation.js"></script>
  <script src="../js/pie_transitions.js"></script>
  <script src="../js/pie_icons.js"></script>
  <script src="../js/basic_legend.js"></script>
  <script src="../js/description_with_arrow.js"></script>
  <script src="../node_modules/chai/chai.js"></script>
  <script src="../node_modules/mocha/mocha.js"></script>

  <link href="../node_modules/mocha/mocha.css" rel="stylesheet" />

</head>
<body>
<div id="mocha"></div>

<script>mocha.setup('bdd')</script>
<script src="expectations.js"></script>
<script src="basic_legend_test.js"></script>
<script src="description_with_arrow_test.js"></script>
<script src="pie_icons_test.js"></script>
<script src="pie_selection_rotation_test.js"></script>
<script src="pie_transitions_test.js"></script>
<script src="rotating_donut_test.js"></script>
<script>
  mocha.checkLeaks();
  mocha.globals(['d3', 'APP']);
  mocha.run();
</script>
</body>

</html>

This test-runner does the following:

  1. Loads the necessary javascript files.  You could load your CSS too if you need that to be tested.
  2. Sets up mocha
  3. Loads our tests
  4. Runs our tests

When you open this file in a browser you get a nice looking interface with the results from all of your tests.

Mocha test results

Basic Test

Let’s take a look at one of our tests for the basicLegend as an example:

describe('BASIC LEGEND', function() {
  it('Allows Multiple Instances', function() {
    var legend1 = APP.basicLegend().key(1);
    var legend2 = APP.basicLegend().key(2);
    chai.assert.notEqual(legend1.key(), legend2.key());
  });
});

describe is a global variable generated by mocha which helps us organize our tests. It takes a title and a function as its arguments. Inside this function is where we’ll run our tests. You can also nest descriptions. it is another global variable created by mocha which is the wrapper for a test. It also takes a title and function.  Inside this are the contents of our first test. Here, we’re creating two instances of our legend and setting the key value of each to a different value. chai is the global created by the chai assertion library. We’re calling the assert method on chai to use the assertion library (as opposed to “should” or “expect”), and then we’re calling the aptly named notEqual method to check to make sure our two instances are in fact, not equal.

Mocha has too many features to review here, but I’d say the best way to learn them would be to start writing some tests.

What to Test

We can test API and DOM only, good thing we broke this up into components

  • Allows multiple instances
  • Methods return expected default values
  • Methods properly set and get values
  • When a component is called on a DOM element, the DOM updates as expected
  • When binding new data to the component, the DOM updates as expected
  • If a method alters the DOM, the DOM updates as expected
  • Events are emitted when they are supposed to
  • Integration

Testing the DOM

Checking that a component has been rendered on the DOM as expected can be a little tricky because different browsers may render things differently enough that comparing the HTML as strings will fail, even though the two are functionally the same. I’d recommend using this function I discovered on Stack Overflow to compare DOM nodes for equality if comparison by string returns unexpected results. Rather than simply comparing innerHTML, it walks through the DOM tree and compares node types, values, and attributes. Here’s an example of how we’d check our DOM against an expected value.

describe('Adds to Dom', function() {
  var node,
      legend;

  var initialData = [
    {id: 1, color: 'red', label: 'item 1'},
    {id: 2, color: 'blue', label: 'item 2'},
    {id: 3, color: 'green', label: 'item 3'}
  ];

  var expected = '<ul class="legend">' +
     '<li class="legend-label" data-id="1" style="left: 12px; opacity: 1; top: 0px;">' +
     '<svg width="22" height="22">' +
     '<rect fill="red" width="20" height="20" x="1" y="1"></rect>' +
     '</svg>' +
     '<span>item 1</span>' +
     '</li>' +
     '<li class="legend-label" data-id="2" style="left: 12px; opacity: 1; top: 22px;">' +
     '<svg width="22" height="22">' +
     '<rect fill="blue" width="20" height="20" x="1" y="1"></rect>' +
     '</svg>' +
     '<span>item 2</span>' +
     '</li>' +
     '<li class="legend-label" data-id="3" style="left: 12px; opacity: 1; top: 44px;">' +
     '<svg width="22" height="22">' +
     '<rect fill="green" width="20" height="20" x="1" y="1"></rect>' +
     '</svg>' +
     '<span>item 3</span>' +
     '</li>' +
     '</ul>'

  beforeEach(function() {
    node = document.createElement('div');
    legend = APP.basicLegend()
        .label(function(d) {return d.label})
        .color(function(d) {return d.color})
        .key(function(d) {return d.id});

    d3.select(node)
        .datum(initialData)
        .transition()
        .duration(0)
        .call(legend);
  });

  it('Initial Data Loads', function(done) {
    var comparisonNode = document.createElement('div');
    comparisonNode.innerHTML = expected;
    setTimeout(function(){
      chai.assert.ok(equivElms(node, comparisonNode));
      done()
    }, 30)
  });
});

function equivElms(elm1, elm2) {
  // returns boolean if elements are functionally the same
  // https://stackoverflow.com/a/10679802
}

We’re running this test asynchronously because it takes some amount of time for the DOM to update. There are more elegant solutions to testing asynchronously, but this way seems fine to me. You simply call the callback function which is passed in as an argument (the callback is called done in the above example).

Integration Testing

I haven’t written any tests for app.js, as that file was really just a showcase for our components, but it would probably make sense to write integrations tests for your application using the same methods as for your components.

Running Our Tests in Node

Running our test suite in a browser is not always the most convenient method, and so it is also possible to run them in node.js. For example, if your team has tests run on gitlab whenever a branch is pushed, then there is no “browser” available. For this, we’ll use mocha with phantomjs. The following are for MacOS, but shouldn’t be too far off for other operating systems.

  1. Download and install node from nodejs.org
  2. Npm comes preinstalled with node, but make sure you’re updated to the latest version
  3. Install phantomjs
  4. Navigate to your project directory
  5. Run npm init.  This will create a file called package.json.
  6. Run npm install mocha-phantomjs-core --save-dev to install mocha-phantom-js in the node_modules directory in your project directory.
  7. To run your test runner in phantomjs, use the command phantomjs ./node_modules/mocha-phantomjs-core/mocha-phantomjs-core.js ./tests/mocha_runner.html. Adjust as necessary for your location of those two files.
  8. Better yet, in your project.json, look for scripts –> test.  Replace the value of that property with the command above.  Then you can use npm test to run your tests.

Complete Code

I would highly recommend reviewing the full diff here so you can clearly see how we added the tests.

You can preview the result by hitting the play button in the header of the embed below. You can also click on the external link icon to open the plunker in a new window.

Next –> Refactoring & Conclusion

Refactoring & Conclusion

Tests <– Previous

This is the final section of our ten part series for large d3.js application development. The series is intended to be followed sequentially, so be sure that you’re read the previous sections before proceeding.

Introduction

We’ve built our application with components that are as independent as possible (good), but that has led to some redundancy among the code (bad). There are a couple of pieces that seem like good candidates for refactoring. The biggest one is the way that methods are built. Here’s the current way that methods are written.

donut.thickness = function(_) {
  if (!arguments.length) {return o.thickness;}
  o.thickness = _;
  return donut;
};

Method Builders

First I’d like to show a pattern that I decided not to use. Because we have the convention of the options object (o in our example) being an object literal with a property for each object, it would be pretty quick to loop through the object and create a method for each object.

function addMethods(options, component) {
  Object.keys(options).forEach(function(key) {
    component[key] = function(_) {
      if (!arguments.length) {return options[key];}
      options[key] = _;
      return component;
    };
  })
}

function someComponent() {
  var o = {
    optionA: 1,
    optionB: 2
  };

  function thing() {
    // some d3 stuff
  }

  addMethods(o, thing)

  return thing;
}

While this gets the job done with minimal code, it introduces some problems. First, it’s no longer obvious which methods are attached to the component. You’d really have to dissect the code before realizing what methods were being added. Second, even if you know which methods belong to the component, it’s not clear where they are being created. This goes against the concept of making the code discoverable. Let’s be a little more explicit in our declarations.

APP.optionMethod = function(key, options, component) {
  return function(_) {
    if (!arguments.length) {
      return options[key];
    }
    options[key] = _;
    return component;
  };
};

function someComponent() {
  var o = {
    optionA: 1,
    optionB: 2
  };

  function thing() {
    // some d3 stuff
  }

  thing.optionA = APP.optionMethod('optionA', o, thing);
  thing.optionB = APP.optionMethod('optionB', o, thing);

  return thing;
}

Now it’s clear where each method is being set, and you can refer to the optionMethod method to get a deeper understanding of what is going on.

We can similarly build helpers for context-specific methods and the on method.

APP.localMethod = function(local, component) {
  function getLocalValue(node) {
    return local.get(node);
  }

  return function(context, _) {
    var returnArray;
    var isList = context._groups[0] instanceof NodeList;
    // if there's do data passed, then we'll return the current selection instead of setting it.
    if (typeof _ === 'undefined' ) {
      returnArray = context.nodes().map(getLocalValue);
      return isList ? returnArray : returnArray[0];
    }
    context.each(function() {local.set(this, _);});
    return component;
  };
};

APP.eventListener = function(events, component) {
  return function(evt, callback) {
    events.on(evt, callback);
    return component;
  };
};


function someComponent() {
  var someLocal = d3.local();
  var events = d3.dispatch('click');

  function thing() {
    // some d3 stuff
  }

  thing.someLocal = APP.localMethod(someLocal, thing);
  thing.optionB = APP.eventListener(events thing);

  return thing;
}

Additional Helpers

In the example above, we’re using the undocumented _groups property of a selection to determine if a selection was called with d3.select or d3.selectAll and return a single value or an array. I’m not super comfortable using an undocumented property, but it’s really the only way to get the functionality I wanted, and it’s fairly semantic, so I’ll be ok with leaving it. We can mitigate the risk of it breaking in the future by only using it once so if it breaks, we only have to fix it in one place.

APP.isList = function(context) {
  return context._groups[0] instanceof NodeList;
};

One other random helper that we used often was the bit that would reuse a transition if the selection was an instance of a transition.  We can refactor this to a separate method.

APP.reuseTransition = function (group, duration) {
  'use strict';
  if (group instanceof d3.transition) {
    return d3.transition(group);
  } else if (typeof duration !== 'undefined') {
    return d3.transition().duration(duration);
  } else {
    return d3.transition();
  }
};

I know that having a helper file which contains a bunch of random helpers probably isn’t the best idea, but hey, they gotta go somewhere.

Adjusting Tests

The functionality of these methods had previously only been testable via its integration in its component. Now, we can test these methods directly. It’s very difficult to test private methods or inner workings of any javascript element, so if you absolutely need to test something, it might be worth exposing them to the public API or moving them to a separate place.

In the code diff here, you’ll see two new test files for method_builders and helpers.

Conclusion

This has been a bit of a passion project for me for quite some time. It started out as building a d3 style-guide to use internally at Beezwax and from there evolved into the depths of existential dread before coming out of the valley of despair with code that I’m actually proud to share.

Once I decided (via Mike Bostock’s advice) that our d3 applications should feel as much like d3 as possible, the end goal became clear. However, it’s been a winding journey to arrive at what I hope seems like the obvious solution. I’ve stripped the git commit history in theory to provide cleaner diffs, but I wasn’t sweating the removal of my earlier missteps. I’m sure it’s not perfect, so if you have any ideas for improvement, let me know and I’ll update the code in this post, plunkers, and on GitHub.

Happy Coding,
– Ryan!

Leave a Reply