12

I'd like to represent the difference between the current data set and the previous data set, as calculated by the client.

Imagine I already have three circles, bound to the data [1, 2, 3]. Now I'd like to update the data and do something based on the difference between the new values and the old?

var new_data = [2, 2, 2]; // This is the new data I'd like to compare with the old

svg.selectAll("circle").data(new_data)
    .transition().duration(2000)
.attr("fill", "red") // e.g. I'd like to colour the circles red if the change
                     // is negative, blue if positive, black if no change.
.attr("r", function(d) { return d * 10; });

Here's a JSFiddle with the above code set into an example.

LondonRob
  • 73,083
  • 37
  • 144
  • 201
  • 1
    D3 doesn't save the old data, so you would have to keep it around and compare yourself. – Lars Kotthoff May 01 '14 at 14:03
  • Any advice on how to best to do this? Is it a good idea, for example, to save the previous data as an attribute on the DOM element? Or is that somehow frowned upon? – LondonRob May 01 '14 at 14:27

2 Answers2

14

You have two options for saving the old data attached to an element in order to identify changes after a new data join.

The first option, as you suggested, is to use data attributes. This SO Q&A describes that approach. Things to consider:

  • all your data values will get coerced to strings
  • you'll need a separate method call/attribute for each aspect of the data
  • you're manipulating the DOM, so it could slow things down if you've got a lot of elements or lot of data for each
  • the data is now part of the DOM, so can be saved with the image or accessed by other scripts

The second option is to store the data as a Javascript property of the DOM object for the element, in the same way that d3 stores the active data as the __data__ property. I've discussed this method in this forum post.

The general approach:

selection = selection.property(" __oldData__", function(d){ return d; } ); 
                        //store the old data as a property of the node
                    .data(newData, dataKeyFunction);  
                        //over-write the default data property with new data
                        //and store the new data-joined selection in your variable

selection.enter() /*etc*/;  

selection.attr("fill",  function(d) {
                 // Within any d3 callback function,
                 // you can now compare `d` (the new data object)
                 // with `this.__oldData__` (the old data object).
                 // Just remember to check whether `this.__oldData__` exists
                 // to account for the just-entered elements.

                if (this.__oldData__) { //old data exists

                  var dif = d.value - this.__oldData__.value; 
                  return (dif) ? //is dif non-zero?
                         ( (dif > 0)? "blue" : "red" ) :
                         "black" ; 
                } else {
                  return "green"; //value for new data
                }

            });

selection.property("__oldData__", null); 
          //delete the old data once it's no longer needed
          //(not required, but a good idea if it's using up a lot of memory)

You can of course use any name for the old data property, it's just convention to throw a lot of "_" characters around it to avoid messing up any of the browser's native DOM properties.

Community
  • 1
  • 1
AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
  • 1
    Genius. Perfect answer as far as I'm concerned. +1 for teaching me why it's not a good idea to write data to the DOM. – LondonRob May 01 '14 at 17:34
  • 2
    Fixed the link. There are times when creating data attributes has benefits -- e.g. see [this Q&A on attaching data to an SVG built server-side](http://stackoverflow.com/a/23096459/3128209) -- but if you don't need it, just using plain Javascript object properties is more efficient. – AmeliaBR May 01 '14 at 17:34
12

As of D3 v4 you can use the built-in support for local variables. The internal implementation is basically the same as suggested by AmeliaBR's answer, but it frees you from having to do the storing of old data on your own. When using d3.local() you can set a value scoped to a specific DOM node, hence the name local variable. In below snippet this is done for each circle by the line

.each(function(d) { previousData.set(this, d) });  // Store previous data locally...

You can later on retrieve that value for any particular node it was stored upon:

.attr("fill", function(d) {
  var diff = previousData.get(this) - d;  // Retrieve previously stored data.
  return diff  < 0 ? "red" : diff > 0 ? "blue" : "black";
}) 

This full code might look something like this:

var old_data = [1, 2, 3]; // When the data gets updated I'd like to 'remember' these values

// Create a local variable for storing previous data.
var previousData = d3.local();

var svg = d3.select("body").append("svg")
    .attr("width", 500)
    .attr("height", 200);

var p = d3.select("body")
  .append("p")
    .text("Old data. Click on the circles to update the data.");

var circle = svg.selectAll("circle")
  .data(old_data)
  .enter().append("circle")
    .attr("fill", "black")
    .attr("r", function(d) { return d * 10; })
    .attr("cx", function(d){ return d * 40; })
    .attr("cy", function(d){ return d * 40; })
    .each(function(d) { previousData.set(this, d) });  // Store previous data locally on each node

svg.on("click", function(d) {
  p.text("Updated data.");
  var new_data = [2, 2, 2]; // This is the new data I'd like to compare with the old
    
  circle.data(new_data)
    .transition().duration(2000)
      .attr("fill", function(d) {
        var diff = previousData.get(this) - d;  // Retrieve previously stored data.
        return diff  < 0 ? "red" : diff > 0 ? "blue" : "black";
      }) 
      .attr("r", function(d) { return d * 10; });
});
<script src="https://d3js.org/d3.v4.js"></script>
Community
  • 1
  • 1
altocumulus
  • 21,179
  • 13
  • 61
  • 84
  • 1
    This seems like the right approach nowadays. Can a third party confirm that this should be my chosen answer? – LondonRob Jan 02 '17 at 21:04
  • As this is a rather old question, I think, you will need to somehow bump it to get some attention or confirmation. It is a new feature introduced by v4 and has the advantage of keeping things confined to the D3 universe. We have just recently seen a similar question ([*"How to access previous data value before binding the new value in d3.js?"*](/q/41265463)), which I think duplicates this one. Hence, I decided to post this as a v4 complement to @AmeliaBR's answer, which is applicable to D3 – altocumulus Jan 02 '17 at 21:25
  • Definitely the way to go as of v4, however the documentation does state ***If you are just setting a single variable, consider using selection.property:*** So the accepted answer could still be considered correct in some cases. – Mike Mellor Jun 28 '17 at 09:52