stimpy
stimpy

Reputation: 33

d3 appears to silently truncate array in .data() method

Working through some D3 examples, I believe have found a d3 bug or behavior I can’t explain .

D3 appears to truncate Arrays when calling a .data() method .

<script>

    d3.csv('medium_january.csv', function(error, data) {
      if (error) {
          console.error('Error getting or parsing the data.');
          throw error;
      }
      var chart = bubbleChart().width(600).height(400);
      d3.select('#chart').data(data).call(chart);
      //here data has 43 elements with the 1st beginning "how flex...
    });

</script>

In bubble chart.js

function bubbleChart() {
    var width = 960,
        height = 960,
        maxRadius = 6,
        columnForColors = "category",
        columnForRadius = "views";

    function chart(selection) {
        var data = selection.enter().data();
//here data has 42 elements with the first being "How I went from zero


        var div = selection,
            svg = div.selectAll('svg');
        svg.attr('width', width).attr('height', height);
        //rest of script

Working example here

https://bl.ocks.org/dmesquita/37d8efdb3d854db8469af4679b8f984a

The problem appears in bubblechart.js

If the number of entries in data in the first block is 43, it becomes 42 in chart() function. The length of selection.enter() is 43 but after .data() we only have 42 entries .

I have tried using different data sets thinking that a bad data may be forcing .data() to fail but got the same results with other data. If if there were bad data i would expect an error and not silently skip.

Why is .data() silently truncating what appears to be the first array value ?

Upvotes: 3

Views: 220

Answers (2)

Gerardo Furtado
Gerardo Furtado

Reputation: 102198

I'm writing this second answer just to show you that Mark's answer is indeed correct and explains what's happening here (I'm not a big fan of argument from authority, but if you click at the D3 top users you're gonna see that Mark is among the all time top answerers... of course nobody is perfect, but it's always a good idea taking the opinion of a top answerer seriously and studying carefully what he/she is saying).

The size of an enter selection

Clearly, your data array has 43 elements. That doesn't change, unless you use a method that modifies that array.

But the issue here is that you're measuring the size of the "enter" selection, not the size of the data array. And data() definitely does not change the size of the data array.

That being said, let's see what is an "enter" selection. This image provide a good explanation:

enter image description here

In a nutshell, the "enter" selection contains all data elements that don't correspond to a DOM element.

In your code, however, the "enter" selection doesn't have the same size of the data array, because, as Mark said, you are binding data to an already existing DOM element. We can show this modifying your code...

d3.select('#chart').data(data).call(chart);

... to this, which is the same:

var sel = d3.select('#chart').data(data);
chart(sel);

Now, using sel, we can compare the size of the data array with the size of the enter selection, before you call `chart:

console.log("the data array has " + data.length + " elements");
console.log("the enter selection has " + sel.enter().size() + " elements");

And this will be the result:

the data array has 43 elements
the enter selection has 42 elements

Here is the modified bl.ocks, have a look at the console: https://bl.ocks.org/anonymous/5204c7ee3e4e15a2b1bbfd7633b1deb6

If you still have any doubt that your first datum is bound to that div, just do...

console.log(d3.select("#chart").datum())

And you'll see this:

Object {
    title: "How Flexbox works — explained with big, colorful, animated gifs", 
    category: "Design", 
    views: "5700"}

Enter selection: a practical example

What confuses a lot of people regarding data() is that it defines the "enter" and "exit" selections based on the elements successfully bound to data.

That being said, people tend to think that, for creating divs, you have to selectAll("div"), or that for creating paragraphs you have to selectAll("p"), as in:

selection.selectAll("p")
    .data(data)
    .enter()
    .append("p")
    etc...

And that's not correct. Actually, if you selectAll("p") before the data() function, you run the risk of selecting any existing paragraph in that page, and your "enter" selection will be shorter than what it should be.

For instance, have a look at this example:

var data = ["foo", "bar", "baz"];
var body = d3.select("body");

var newDivs = body.selectAll(null)
  .data(data)
  .enter()
  .append("div")
  .html(function(d){ return d})
<script src="https://d3js.org/d3.v4.min.js"></script>
<div>Hello</div>

As I'm using selectAll(null), my "enter" selection has all the elements of my data array, regardless the fact that there is already a div in that page.

Now, have a look at this other example:

var data = ["foo", "bar", "baz"];
var body = d3.select("body");

var newDivs = body.selectAll("div")
  .data(data)
  .enter()
  .append("div")
  .html(function(d){ return d})
<script src="https://d3js.org/d3.v4.min.js"></script>
<div>Hello</div>

As you can see, due to the fact that there is already a div in that page, when I do selectAll("div") before the data() function, my first data element ("foo") is bound to that div, and my "enter" selection end up having only two divs.

Upvotes: 2

Mark
Mark

Reputation: 108557

What's happening here is that since you use .select('#chart').data(data) the first item in your array is being data-bound to your div. .enter() is then just returning the rest of the array. This is a strange way to use data-binding. Essentially, all you are after is a way to pass your data into your function. This could be accomplished much more conventionally as:

d3.select('#chart').call(chart, data);

Where your function definition becomes:

function chart(selection, data) {
    var div = selection,
        svg = div.selectAll('svg');
    svg.attr('width', width).attr('height', height);

    ...

Update block.

Response to Comment

Apologies if I was unclear. You have an element with an id of chart on the page. You then bind your data to it. Since the element exists the first item in your array is bound to it. You then .enter() the selection, this returns the rest of the array, representing items which do not have existing elements. Does that make sense?

Upvotes: 3

Related Questions