Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3.js v4: How to access parent group's datum index?

Tags:

d3.js

The description of the selection.data function includes an example with multiple groups (link) where a two-dimensional array is turned into an HTML table.

In d3.js v3, for lower dimensions, the accessor functions included a third argument which was the index of the parent group's datum:

td.text(function(d,i,j) {
  return "Row: " + j;
});

In v4, this j argument has been replaced by the selection's NodeList. How do I access the parent group's datum index now?

like image 759
Oliver Avatar asked Jul 06 '16 20:07

Oliver


4 Answers

Well, sometimes an answer doesn't provide a solution, because the solution may not exist. This seems to be the case.

According to Bostock:

I’ve merged the new bilevel selection implementation into master and also simplified how parents are tracked by using a parallel parents array.

A nice property of this new approach is that selection.data can evaluate the values function in exactly the same manner as other selection functions: the values function gets passed {d, i, nodes} where this is the parent node, d is the parent datum, i is the parent (group) index, and nodes is the array of parent nodes (one per group). Also, the parents array can be reused by subselections that do not regroup the selection, such as selection.select, since the parents array is immutable.

This change restricts functionality—in the sense that you cannot access the parent node from within a selection function, nor the parent data, nor the group index — but I believe this is ultimately A Good Thing because it encourages simpler code.

(emphasis mine)

Here's the link: https://github.com/d3/d3-selection/issues/47

So, it's not possible to get the index of the parent's group using selection (the parent's group index can be retrieved using selection.data, as this snippet bellow shows).

var testData = [  [    {x: 1, y: 40},    {x: 2, y: 43},    {x: 3, y: 12},    {x: 6, y: 23}  ], [    {x: 1, y: 12},    {x: 4, y: 18},    {x: 5, y: 73},    {x: 6, y: 27}  ], [    {x: 1, y: 60},    {x: 2, y: 49},    {x: 3, y: 16},    {x: 6, y: 20}   ]   ];    var svg = d3.select("body")  	.append("svg")    .attr("width", 300)    .attr("height", 300);      var g = svg.selectAll(".groups")      .data(testData)      .enter()      .append("g");        var rects = g.selectAll("rect")      .data(function(d, i , j) { console.log("Data: " + JSON.stringify(d), "\nIndex: " + JSON.stringify(i), "\nNode: " + JSON.stringify(j)); return d})      .enter()      .append("rect");
<script src="https://d3js.org/d3.v4.min.js"></script>
like image 98
Gerardo Furtado Avatar answered Oct 07 '22 10:10

Gerardo Furtado


My workaround is somewhat similar to Dinesh Rajan's, assuming the parent index is needed for attribute someAttr of g.nestedElt:

v3:

svg.selectAll(".someClass")     .data(nestedData)   .enter()   .append("g")     .attr("class", "someClass")   .selectAll(".nestedElt")     .data(Object)   .enter()   .append("g")     .attr("class", "nestedElt")     .attr("someAttr", function(d, i, j) {      }); 

v4:

svg.selectAll(".someClass")     .data(nestedData)   .enter()   .append("g")     .attr("class", "someClass")     .attr("data-index", function(d, i) { return i; }) // make parent index available from DOM   .selectAll(".nestedElt")     .data(Object)   .enter()   .append("g")     .attr("class", "nestedElt")     .attr("someAttr", function(d, i) {       var j = +this.parentNode.getAttribute("data-index");     }); 
like image 32
Zim Avatar answered Oct 07 '22 12:10

Zim


I ended up defining an external variable "j" and then increment it whenever "i" is 0

example V3 snippet below.

rowcols.enter().append("rect")
 .attr("x", function (d, i, j) { return CalcXPos(d, j); })
 .attr("fill", function (d, i, j) { return GetColor(d, j); })

and in V4, code converted as below.

var j = -1;
rowcols.enter().append("rect") 
 .attr("x", function (d, i) { if (i == 0) { j++ }; return CalcXPos(d, j); })
 .attr("fill", function (d, i) { return GetColor(d, j); })
like image 21
Dinesh Rajan Avatar answered Oct 07 '22 12:10

Dinesh Rajan


If j is the nodeList...

  • j[i] is the current node (eg. the td element),
  • j[i].parentNode is the level-1 parent (eg. the row element),
  • j[i].parentNode.parentNode is the level-2 parent (eg. the table element),

  • j[i].parentNode.parentNode.childNodes is the array of level-1 parents (eg. array of row elements) including the original parent.

So the question is, what is the index of the parent (the row) with respect to it's parent (the table)?

We can find this using Array.prototype.indexOf like so...

k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode);

You can see in the snippet below that the row is printed in each td cell when k is returned.

var testData = [
[
  {x: 1, y: 1},
  {x: 1, y: 2},
  {x: 1, y: 3},
  {x: 1, y: 4}
], [
  {x: 2, y: 1},
  {x: 2, y: 2},
  {x: 2, y: 3},
  {x: 2, y: 4}
], [
  {x: 3, y: 4},
  {x: 3, y: 4},
  {x: 3, y: 4},
  {x: 3, y: 4}
 ]
];

var tableData =
  d3.select('body').selectAll('table')
    .data([testData]);

var tables =
  tableData.enter()
  .append('table');

var rowData =
  tables.selectAll('table')
    .data(function(d,i,j){
      return d;
    });

var rows =
  rowData.enter()
  .append('tr');

var eleData =
  rows.selectAll('tr')
    .data(function(d,i,j){
      return d;
    });

var ele =
  eleData.enter()
  .append('td')
    .text(function(d,i,j){
      var k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode);
      return k;
    });
<script src="https://d3js.org/d3.v4.min.js"></script>

Reservations

This approach is using DOM order as a proxy for data index. In many cases, I think this is a viable band-aid solution if this is no longer possible in D3 (as reported in this answer).

Some extra effort in manipulating the DOM selection to match data might be needed. As an example, filtering j[i].parentNode.parentNode.childNodes for <tr> elements only in order to determine the row -- generally speaking the childNodes array may not match the selection and could contain extra elements/junk.

While this is not a cure-all, I think it should work or could be made to work in most cases, presuming there is some logical connection between DOM and data that can be leveraged which allows you to use DOM child index as a proxy for data index.

like image 43
Steve Ladavich Avatar answered Oct 07 '22 12:10

Steve Ladavich