Skip to content

CUFP 2014 Tutorial Reusable Charts Pattern

seliopou edited this page Sep 3, 2014 · 29 revisions

Wiki ▸ [CUFP 2014 Tutorial](CUFP 2014 Tutorial) ▸ Reusable Charts Pattern

The goal of D3.js is offer an alternative to imperative models of DOM manipulation, and by extension front-end development. Rather than specifying the incremental modifications that should be applied to a document, D3.js allows you to write code that describes what your document should look like based on the data of your application. In this sense it is a library that promotes a declarative style. Yet out of the box it lacks certain compositional properties.

This section of the tutorial will introduce a simplified version of the "reusable charts" pattern that the D3.js community uses to promote the reuse of code. The pattern will then be pushed down all the way to the level individual operations, which will imbue the library with much nicer compositional properties and become the basis of the elm-d3 bindings.

Delaying Execution

Consider the following code:

d3.selectAll('div')
    .attr('class', 'container');

While this code does do something useful, it has a few undesirable properties that prevent it from being reusable. First and foremost, this code executes immediately if included in a top-level JavaScript application. This leaves you no opportunity to programmatically manipulate and extend the operations further. By wrapping the operation in a function, you can delay its execution. In addition, doing this allows you to programmatically decide which additional operations—if any—should be to the selection.

var div_op1 = function() {
  return d3.selectAll('div')
      .attr('class', 'container')
}

To execute the code, you just call the function. This will return a selection that you can apply further operations to:

div_op().style('background-color', 'magenta');

Generalizing the Context

Another limitation of the operation above is that you have no control over which subtree of the document it will apply to; implicit in the selectAll operation is that it will apply to the root element of the document. The original code is roughly equivalent to the following:

d3.select(document).selectAll('div')
    .attr('class', 'container');
var div_op2 = function(selection) {
  return selection.selectAll('div')
      .attr('class', 'container');
}

You can now apply div_op2 to the entirety of the document, while also extending it with other operations:

div_op2(d3.select(document)).attr('background-color', 'teal');

You can now also apply it to a specific subtree of the document:

div_op2(d3.select('#sidebar')).attr('background-color', 'yellow');

# selection.call(function)

Apply the provided function to the selection. This is equivalent to function(selection).

Combinators

function chain(op1, op2) {
  return function(selection) {
    return op2(op1(selection));
  };
}
function seq(op1, op2) {
  return function(selection) {
    op1(selection);
    return op2(selection);
  };
}

Exercises

Translate the following D3.js code into the declarative style described above. Use <;> for the seq operator, and imagine that JavaScript has a left-associative |. infix operator, where expr1 |. expr2 is equivalent to chain(expr1, expr2). Then write the final line of code that will execute the operations on the root of the current document. It may help readability if name intermediate subexpressions.

var rect = d3.selectAll('rect')
    .data([8, 3, 15, 23, 12]);

rect.enter().append('rect')
    .style('fill', 'steelBlue')
    .attr('x', 10)
    .attr('height', 25);

rect
    .attr('y', function(d, i) { return i * 30 + 30; })
    .attr('width', function(d, i) { return (480 - 20) * d.value / max; });

Additional Reading

Next: D3.js in Elm