-
Notifications
You must be signed in to change notification settings - Fork 36
CUFP 2014 Tutorial Reusable Charts Pattern
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.
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');
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)
.
function chain(op1, op2) {
return function(selection) {
return op2(op1(selection));
};
}
function seq(op1, op2) {
return function(selection) {
op1(selection);
return op2(selection);
};
}
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; });