Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compare/Diff new data with previous data on d3.js update

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.

like image 953
LondonRob Avatar asked May 01 '14 14:05

LondonRob


People also ask

Is D3 js obsolete?

As you can see, there's numerous reasons as to why D3 is fairly outdated now for many common use cases. The web has evolved significantly since its release. If you're doing simple charts like donuts, bar charts, line charts, scatter plots, etc, consider seeing if you can implement them using your existing framework.

What is better than D3 JS?

We have compiled a list of solutions that reviewers voted as the best overall alternatives and competitors to D3js, including Syncfusion Essential Studio Enterprise Edition, Chart. Js, Angular, and DevExpress.

What is D3 js good for?

D3 is a JavaScript library and framework for creating visualizations. D3 creates visualizations by binding the data and graphical elements to the Document Object Model. D3 associates (binding) the data (stuff you want to visualize) with the DOM. This allows the user to manipulate, change or add to the DOM.


2 Answers

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.

like image 196
AmeliaBR Avatar answered Sep 30 '22 13:09

AmeliaBR


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>
like image 36
altocumulus Avatar answered Sep 30 '22 13:09

altocumulus