Link to wealthfront.com

Fork me on GitHub

Thursday, April 18, 2013

Reactive Charts with D3 and Reactive.js

New to Reactive.js? Check out last week's introduction.

All visualizations are ultimately a composition of smaller elements; data sets, scales, individual lines, and labels to name a few. Those elements relate to each other in different ways, and to manage their interdependency we typically find ourselves writing a master "render" method — something that can take new data and re-draw the visualization in its entirety, imperatively recalculating each component.

At Wealthfront we've been using Reactive.js to do things a little differently. We describe our visualizations as a flow of information, not as an imperative set of rendering steps. To illustrate this, we'll build up a simple bar chart that shows how Reactive.js can change the way you write, and interact with your visualizations — hopefully for the better! When we're done, our humble chart will look a little something like this:


First, we need to declare each component of our chart and how those components relate to each other, so let's decompose our chart into the values that describe it.

Width and Height


Width and Height represent the width and height of our chart in pixels. Since they're just simple values, we'll represent them with $R.state(). Remember that $R.state() just returns a reactive function that lets you store and retrieve a value.  We'll set the width and height to 200px by default.


Data

"[{name:foo, value:10}, {name:bar, value:20}]"


Our data will be a simple array of object literals. Each object will have a name and value. Our chart will need to consume this data and render the bars accordingly. Since this is another simple literal value, we'll use $R.state() again, and default to an empty array.


Y Scale


In D3 we use scales to map the values in our data set to actual pixels on the screen. We do this by specifying a domain (the max and min of our data) and a range (the max and min dimensions of our chart in pixels). In this case our Y scale will need to have access to the chart's height, and our data, so that it can create a scale that maps our data to proper pixel heights on the screen.

We'll define our Y scale as a function, and then "reactify it" so that it can represent the Y scale value in our visualization. Since our Y scale needs access to our chart's height and data, we'll need to bind it to those values so that they can update our scale when they change.


X Scale


Our X scale behaves similarly, we'll use an ordinal scale to map our discrete bars onto actual X positions and widths on the screen. Our scale will need our data, and our chart's width to determine how things should map. We'll also use the rangeBands feature of D3's ordinal scales to figure out how wide each bar should be.


Bars


Now we need a value that actually represents the SVG Rectangles that will make up our bars. If you've used D3 before, the code below should look pretty familiar — it follows the standard D3 process to add, update, and remove bars from our chart. Our bars need the SVG group that contains them, our data, the chart's height, and the x and y scales to map our data values to actual coordinates. We define the function, reactify it, and bind it to the relevant values.


Labels


Last but not least, we need to represent our labels. These will be SVG Text elements, contained in their own labels group. Like our bars, they'll need their container, as well as our data, x, and y scales.


All together now

This JSFiddle shows our example functioning in its entirety. The inputs allow you send values to chart.width(), chart.height() and chart.data(). Those three reactive values that we exposed on our BarChart object become our API. When we assign new values to them, data flows through our graph of values, changing the chart's representation in the process.



Because we expressed how data flows through the various components that make up our visualization, there's no need for a master "render" method, we allow the chart to update itself. Note that only the parts of our chart affected by a given change will be updated. In our example that's everything, but in large visualizations the targeted updates Reactive.js provides let us avoid unnecessary re-rendering of untouched elements. The best part is, we get that behavior for free.

Hopefully this provides a small, tangible example of how thinking reactively can inform the design of your code. Stay tuned in the weeks to come as we address topics like reactive UIs, AJAX requests, and other fun applications!