Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Loop through array of arrays in D3

I have an array of x objects each with an array of y objects like so:

var data = [
    { name: '1', data: [1,2,3] },
    { name: '2', data: [1,2,3] }
]

Is there a way to chain d3 functions in order to iterate through each data[x] and each data[x][y]?

I thought something like this would have helped, but not apparently:

var svg = d3.selectAll('g')
    .data(data)
    .enter()
    .append('g')
    .selectAll('rect') /* This might be made up, but it makes sense to me :)*/
    .data(data, function (d, i) {
        return data[i].data;
    })
    .enter()
    .append('rect')

Thanks

like image 717
Ade Avatar asked Mar 01 '16 16:03

Ade


3 Answers

What about something along these lines : //untested

var svg = d3.selectAll('g')
    .data(data)
    .enter()
    .append('g')
    .selectAll('rect') /* This might be made up, but it makes sense to me :)*/
    //.data(data, function (d, i) {
    //   return data[i].data;
    //})
    //.enter()
    //.append('rect')
    .each(function(d){
    d3.select(this).selectAll('rect').data(d.data).append('rect')//and so on
    })

EDIT :

Working fiddle : http://jsfiddle.net/reko91/58WJE/34/

var data = [
    { name: '1', data: [1,2,3] },
    { name: '2', data: [1,2,3] }
]

var colourScale = ['red','blue','yellow']
var svg = d3.select('body').append('svg').attr('width', 1000).attr('height', 1000);
svg.selectAll('rect')
    .data(data)
  .enter().append('g')
    .each(function(d,i){
        d3.select(this).selectAll('rect')
        .data(d.data)
      .enter().append('rect')
        .attr('x', function(d,j) { return j*100; })
        .attr('y', function() { return i*100; })
        .attr('width', function() { return 90; })
        .attr('height', function() { return 90; })
        .attr('fill', function(d,j) { console.log(d); return colourScale[j]; })
    });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
like image 52
thatOneGuy Avatar answered Nov 09 '22 19:11

thatOneGuy


While an explicit .each loop works, the d3 approved way to do this is with a nested selection:

var data = [
    { name: '1', data: [1,2,3] },
    { name: '2', data: [1,2,3] }
]

var colourScale = ['red','blue','yellow']
var colourScale2 = ['yellow','pink','black']
var svg = d3.select('body').append('svg').attr('width', 1000).attr('height', 1000);
svg.selectAll('rect')
    .data(data)
  .enter().append('g')
  .selectAll('rect')
  .data(function(d){
    return d.data;
  })
  .enter()
  .append('rect')
  .attr('x', function(d,i) { return i*100; })
  .attr('y', function(d,i,j) { return j*100; })
  .attr('width', function() { return 90; })
  .attr('height', function() { return 90; })
  .attr('fill', function(d,i) { return colourScale2[i]; })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
like image 44
Mark Avatar answered Nov 09 '22 19:11

Mark


This is a good question about "How to nest elements with nested data in d3.js", which I also encounter recently. (also thinking a robust way to deal with these scenario)

These articles wrote by Mike Bostock related to the issue:
1. How selection work
- let you know how data binding to selection
2. Nested Selection
- knowing how selection chaining function really happen under the hood

Here is how I deal with this problem when using svg (canvas may have other ways).

First, the mindset of D3 is 'Data-driven Document'. To deal with nested data, you have to create nested elements in SVG.(you will find that your design on data structure really matter, it reflect viz result). < g > element(g stand for grouping) in SVG can solve this problems, which created for grouping elements under svg like < path > , < rect >, < circle >. You cannot nested element under path, rect, circle but g or svg.

Second, you need to imagine the nested structure on document. Here for example:

<svg>
    <g class="layer1">
        <g class="layer2">
            <rect x="0" y="1" class='layer2'></rect>
        </g>
        <g class="layer2">
            <rect x="1" y="1" class='layer2'></rect>
        </g>
        <g class="layer2">
            <rect x="2" y="1" class='layer2'></rect>
        </g>
    </g>
    <g class="layer1">
        <g class="layer2">
            <rect x="0" y="2" class='layer2'></rect>
        </g>
        <g class="layer2">
            <rect x="1" y="2" class='layer2'></rect>
        </g>
        <g class="layer2">
            <rect x="2" y="2" class='layer2'></rect>
        </g>
    </g>
</svg>

Third, using the d3.collection and d3.select to match the data and document. Better to read the d3 doc, in case the change of syntax under different d3 version. The d3.v4 and v3 had quite a difference under the data join, select, data index, data manipulation and now 2018/02 the d3 go into v5.

Here re-frame the question code.

const svg = d3.select('body')
              .append('svg')
              .attr('width', 1000)
              .attr('height', 1000);

We got the svg, which is d3.select obj, contain the svg element.

const layer1 = svg.selectAll('g.layer1')
                  .data(data)
                  .enter()
                  .append('g')
                  .classed('layer1', true);

We got the layer1, which created by using selectAll and enter our data with .data() and enter(), also append the g element according to the object num of data, which is 2 in this scenario

const layer2 = layer1.selectAll('g.layer2')
                     .data((d,i,j)=>{
                        let num = d3.entries(j)[i].key
                        return d.data.map((d)=> [d,num])
                                     })
                     .enter()
                     .append('g')
                     .classed('layer2', true);

We got the layer2 element, leveraging the hierarchical selection ability of d3.selection in chaining function and use the d3.enteries to give the layer1 index pass to layer2 (since v4, the data key function will no longer keep parent index, so this is the alternative method i figure out. refer to this discussion). the d3.enteries give the key to each element in array, we use it to store the parent index

layer2.append('rect')
      .attr('x', (d,i)=> i*60)
      .attr('y', (d,i)=> d[1]*60)
      .attr('width', 50)
      .attr('height', 50)
      .attr('fill', 'blue');

Then, Using layer2 element, we can append rect element according to the array in data.data with the index information of each data element .By the way, in this solution, we let each element in data.data carry the parent index.

So the result can be following: enter image description here

šŸ€† šŸ€† šŸ€†
šŸ€† šŸ€† šŸ€†  

demo on Observable:
https://beta.observablehq.com/@weitinglin/simple-case-01-nested-data-with-nested-selection

like image 1
Weiting Lin Avatar answered Nov 09 '22 19:11

Weiting Lin