Visualization Multiples: Thinking With D3

This is a step-by-step example to build an alternative visualization to the “trellis” available in SAP Lumira or other SAP products that use D3 for visualization.  It’s also an introduction to the D3-specific idiom for grouped visualizations.  Rather than using a conventional loop around the outside of a visualization, D3 prefers to structure its loops passed in as data, as in “data-driven”.


This blog covers a portion of my SAP TechEd 2015 presentation.


Trellis Visualization Concept

You can see the Lumira trellis here, using a donut chart. Note that we can only fit four charts on the screen before they start going off-screen. That makes it difficult to compare Q1 last year to Q1 this year – you can’t see them at the same time, and the slider bar at the bottom is an awkward workaround. They want a more sensible arrangement of four quarters across, arranged so one row is one year.


TrellisBlogImage1.jpg

Conventional Thinking

As a programmer, what do you do when you need to take one object and make several of them?  Easy, you just put a loop around it.  Maybe add a level of abstraction and some parameters, call it as a function n times. Something like this:

  • Split the data into sets by quarter
  • Two layers of loops (4 across, 2 down)
  • Abstract out all my single objects to array of objects.

The first time I ran into this problem, that’s exactly what I started to do.  I nested my data by quarter using D3’s awesome nest() function.  Then I got the unique values from the data set using D3 keys() function.  Doing this, I felt like I was thinking in D3.  Then I started to write two layers of loops …but wait.

TrellisBlogImage2.jpg

That seems wrong.  Reading the code of the many visualizations in the D3 gallery, one of the striking things I’d noticed is the absence of “for” loops.  You hardly ever see one, and certainly not two in the same visualization.

So if we’re not going to use a “for” loop, how can we make multiples of the visualization?  We’ll use data.  For SQL and Business Intelligence people, the best analogy is that we “join” the data with another data set to make it multiply like a Cartesian.


Step 1: Multiplying with Data

In the table below, here is the code for a single pie chart (or donut). I start out passing in just one quarter of data.

What does this code do?  It creates a collection of arcs by passing in my data through D3’s built-in pie function.

  • The pie() function translates the data into the start and end points of arcs or wedges. 
  • The selectAll() “joins” that data with vis (the visualization object, loosely speaking, like a canvas object).  It’s not precisely a “join” but it’s useful as a term that is familiar to Business Intelligence users who are the main audience for Lumira
  • The enter() function defines what happens when these get drawn on the screen, when they enter the screen. 
  • Upon entering the screen, the arcs get a g element (grouping element) with the class “arc”.  It’s good practice to attach classes to your objects.  When you think with D3, that class label is your handle to grab the object. 

Single Pie

Multi-Pie

var g = vis.selectAll(“.arc

       .data(pie(dataQ1))

       .enter().append(“g”)

       .attr(“class”, “arc”);

var mpie = vis.selectAll(“.mpie”)

.data([0,1,2,3])

.enter().append(“g”)

.attr(“class”, “mpie”);

var g = mpie.selectAll(“.arc”)

      .data(pie(dataQ1))

      .enter().append(“g”)

      .attr(“class”, “arc”);

Now let’s look at the Multi-Pie code. We will call the smaller pie charts mpie, for multi-pie. 

  • The first selectAll() statement joins with the static array 0-1-2-3.
  • This creates I have four mpie-class elements on the vis object. 
  • The JavaScript object mpie is an array of those four objects.
  • In the second selectAll() statement, arcs from the pie() function get added to the mpie elements

Notice that we’re no longer adding the arcs to the vis, the main visualization object, but to the four mpie elements instead.  D3 has this multiplicity built in.


After I do this, there are four charts on the page, stacked on top of each other.  But it looks like just one pie chart!  How can I tell if it’s working?

TrellisBlogImage3.jpg

Right-click on the chart and choose Inspect Element. That brings up the Elements tab, where you will see your vis object containing four mpie-class objects.

TrellisBlogImage4.jpg

TrellisBlogImage5.jpg

Step 2: Unstacking

Now that we have four charts, we want to un-stack them and see them side-by-side. We resize the charts by dividing height by 2, width by 2. (I don’t show you code for that)


Single Pie

Multi-Pie

var mpie = vis.selectAll(“.mpie”)

         .data([0,1,2,3])

         .enter().append(“g”)

         .attr(“class”, “mpie”);

var locs = [[0,0],

           [0,plotHeight],

           [plotHeight, 0],

           [plotHeight, plotHeight]];

var mpie = vis.selectAll(“.mpie”)

.data([0,1,2,3])

.enter().append(“g”)

.attr(“class”, “mpie”)

.attr(“transform”, function(d){  

          return “translate(“+locs[d][0]+

                  “,” + locs[d][1] + “)”;});

Then we add two more lines of code to spread out those four charts. The first line creates an array locs, to define coordinates where we want the charts.


The other code addition, transform, tells D3 to “slide” the four charts to those coordinates.  It’s a “d” function – for each item d in the data set, which is 0-1-2-3, we pick up the right location for the chart.  Note the math term “translate” in the transform function – “push” or “slide” is a clearer word.


Now we have four identical charts.

TrellisBlogImage6a.jpg

and you can see the transformation in code by Inspect Element.  Contrast this with the Inspect Element from Step 1.


TrellisBlogImage7.jpg


Step 3: Separate Data

At this point, we want different data in each graph.  Remember we started out with just one quarter of data. 


Now we add the quarter column to the data set, and adjust the pie function to step through all four quarters of the data.  To get the data split by quarter, we need a nest() statement. I want each data element from nest() use a consistent name, so I’ll be using nest with the entries() method (not map()).


Single Pie

Multi-Pie

var g = mpie.selectAll(“.arc”)

      .data( pie(dataQ1)                          

           )

      .enter().append(“g”)

      .attr(“class”, “arc”);

var dataNested = d3.nest()

   .key(function(d){return d.Quarter;}

   .entries(data);

var g = mpie.selectAll(“.arc”)

       .data(function (d,i) {

          return pie(dataNested[d].values);})

       .enter().append(“g”)

       .attr(“class”, “arc”);

Inside our mpie-drawing, we used to just pass the variable dataQ1.  Now we have to pass in a subset of the data; and to define it we’re using another d-function. The chart requires the values under the quarter’s key – dataNested[d].values.  That’s terrible to explain in words, but a table helps a lot:


dataNested[‘FY15Q2’].key

dataNested[‘FY15Q2’].values

dataNested[‘FY15Q2’].entries

“FY15Q2”

{

    Object 0: {

    Quarter: “FY15Q2”

    Region: “China”

    Sales: 4568.08

     }

    Object 1 {}

    Object 2 {}

    …

}

{

Key: “FY15Q2”

Values: {

    Object 0: {

    Quarter: “FY15Q2”

    Region: “China”

    Sales: 4568.08

     }

    Object 1 {}

    Object 2 {}

    …

   }

}

At the moment, the d variable inside mpie.selectAll() is stepping through the data we joined to mpie in step 1 – [0,1,2,3].  That takes advantage of JavaScript’s flexible syntax, where you could reference by number [0] or by a text associative array [FY15Q2].  That array notation isn’t very common in SQL or other syntax familiar to Business Intelligence users, but at least one thing is: the dot notation indicating levels. Schema.table.column in SQL traverses the objects in a database in the same way dataNested.entries[0].Sales traverses a data structure to return a numeric values for sales.


In that last example, you might notice that the “d” function also has an “i” in it: function (d,i) {}. Why? No reason except to be instructive for new D3 users.  The convention is that d steps you through the data elements you’re joining to while i provides a numeric index.


Now that we added these two code elements, we have four charts with four different sets of data.

TrellisBlogImage8.jpg

Step 4: Exact User Requirements

Now here’s our final step.  The users didn’t want four charts.  They wanted four from last year, and the current year which is two quarters so far. You’ll have to resize your charts again to fit four across (not shown).


Then we replace our static array with the quarters.  Those are just the unique values from the nested data; we can get them with the built-in D3 function keys().

Single Pie

Multi-Pie

var locs = [[0,0],

           [plotHeight, 0],

           [0,plotHeight],

           [plotHeight, plotHeight]];

var mpie = vis.selectAll(“.mpie”)

         .data([0,1,2,3])

         .enter().append(“g”)

         .attr(“class”, “mpie”)

         .attr(“transform”, function(d){  

          return “translate(“+locs[d][0]+

                  “,” + locs[d][1] + “)”;});

var locs = [[0,0],

           [plotHeight,0],

           [2*plotHeight,0],

           [3*plotHeight,0],

           [0,plotHeight, 0],

           [plotHeight, plotHeight]];

var quarters = d3.keys(dataNested);

var mpie = vis.selectAll(“.mpie”)

         .data(quarters)

         .enter().append(“g”)

         .attr(“class”, “mpie”)

         .attr(“transform”, function(d){  

          return “translate(“+locs[d][0]+

                  “,” + locs[d][1] + “)”;});

So we replace the static array with our array of quarters, and flesh out our array of locations.  Now we have our yearly series like the users requested.


TrellisBlogImage9a.jpg


D3 Chart Multiplicity (for SQL People)


Summing up in terms familiar to Business Intelligence and SQL users:

  • Reference your data in a nested structure from d3.nest()
  • You should require only a few small changes inside the code that built the single chart.  You won’t need loops around the outside.
  • You can think of multiplying charts is a “join” to another data set, just like rows multiply in a report.  It’s handy to use a temporary static array as a “join” for troubleshooting or development.
  • Use Inspect-element in your browser to troubleshoot stacked or “missing” elements.
  • JavaScript dot-notation is very similar to SQL.
  • An easy place to create a bug is in the array notation, which can be index-based [0] or associative [“FY15Q1”] using the same syntax [d].  Pay close attention here.


The code for these examples is attached below.  Due to restrictions on file types available for upload, you will have to rename the .js files and .csv files for your operating system.

To report this post you need to login first.

1 Comment

You must be Logged on to comment or reply to a post.

  1. Arvind Padmanabhan

    Excellent tutorial. Thanks. In my case, each chart data comes from a different JSON file. I am reading the data in this manner:

    d3.json(“data0.json”, function(error, data) {

        x.domain(data.map(function(d) { return d.ball; }));

        y.domain([0, d3.max(data, function(d) { return d.value; })]);

        …

    }

    At the moment, I am simply duplicating code. Can you suggest how to incorporate your method? Thanks.

    (0) 

Leave a Reply