To see this in practice, let's take a look at the scripts/viz.js program as a template for creating testable code for the data manipulation functions in the visualization. For this example, we will create a set of simple bars that are based on the profit of an arbitrary dataset. We are given the sales and cost in the data; however, we need to determine the profit for the visualization by subtracting the sales from the cost. In this contrived example, we need a few small helper functions, which are as follows:
- A function to take the original dataset and return a new dataset with the profit calculated
- A function to retrieve an array of unique categories to apply to an ordinal scale
- A function to determine the maximum profit value in order to build the upper bound of our input domain
If we create these functions with the best practices outlined earlier and expose them externally, we can test them in isolation and independently.
Let's take a tour of the script to see how it all works together:
if (d3.charts === null || typeof(d3.charts) !== 'object')
{ d3.charts = {}; }
Here, we will define the namespace for the chart. In this example, our chart can be instantiated with d3.charts.viz. If the d3 object with the charts property does not exist, or if it is not of the type object, create it, using classical functional inheritance to leverage common patterns from a base function:
d3.charts.viz = function () {
// Functional inheritance of common areas
var my = d3.ext.base();
A handy function (see base.js) to quickly assign getters/setters to the closure following the pattern in Towards Reusable Charts is as follows:
// Define getter/setter style accessors..
// defaults assigned
my.accessor('example', true);
We use the svg variable at this level of scope to maintain state when quickly appending selectors. The void 0 is a safer way to initialize the variable as undefined:
// Data for Global Scope
var svg = void 0,
chart = void 0;
Define the D3 instance functions that will be used throughout the visualization:
// Declare D3 functions, also in instance scope
var x = d3.scale.linear(),
y = d3.scale.ordinal();
The following function represents the main interface to the outside world. There is also a set of setup functions commonly seen in D3 visualizations. The SVG container is set up in a way that can easily look for existing SVG containers in the selector and rebind the data. This makes it much easier to redraw when making subsequent calls with new data:
my.draw = function(selection) {
selection.each(function(data) {
// code in base/scripts.js
// resuable way of dealing with margins
svg = my.setupSVG(this);
chart = my.setupChart(svg);
// Create the visualization
my.chart(data);
});
};
// main method for drawing the viz
my.chart = function(data) {
var chartData = my.profit(data);
x.domain([0, my.profitMax(chartData)])
.range([0,my.w()]);
y.domain(my.categories(chartData))
.rangeRoundBands([0, my.h()], 0.2);
var boxes = chart.selectAll('.box').data(chartData);
// Enter
boxes.enter().append('rect')
.attr('class', 'box')
.attr('fill', 'steelblue');
// Update
boxes.transition().duration(1000)
.attr('x', 0)
.attr('y', function(d) { return y(d.category) })
.attr('width', function(d) { return x(d.profit) })
.attr('height', y.rangeBand())
// Exit
boxes.exit().remove();
};
Notice that the chart function relies on several helper functions (shown in the following lines of code) to work with the data. It is also written in such a way that we can take advantage of the enter/update/exit pattern:
// Example function to create profit.
my.profit = function(data) {
return data.map(function(d) {
d.profit = parseFloat(d.sales) - parseFloat(d.cost);
return d;
});
};
This function is used to create a new data structure that has profit assigned. Note that it takes one data array in as a parameter and returns a newly constructed array with the profit attribute added. This function is now exposed externally with viz().profit(data) and can be easily tested. It does not change any of the outside global variables. It is just data in and new data out:
my.categories = function(data) {
return data.map(function(d) {
return d.category;
});
};
This is the exact same pattern as my.profit(data). We will take the data structure in as input and return a new data structure, that is, an array of all the categories. In the preceding lines of code, you saw that this is leveraged to create the input domain.
my.profitMax = function(data) {
return d3.max(data, function(d) { return d.profit; });
};
Once again, a simple function to take data in, compute the max, and return that maximum value. It is very easy to test and verify with d3.charts.viz().profitMax(data)?
return my; };