2

I have a method on a reusable chart that can be passed a selection and return a value if it is passed a d3.select('#id') selection or an array of values if it is passed a d3.selectAll('.class') selection. I'm currently interrogating the passed argument with context._groups[0] instanceof NodeList, but it feels a little fragile using an undocumented property, as that may change in future versions. Is there a more built in way of determining if a selection comes from select or selectAll?

selection.size() will not help here, as it only tells us the result of the selection, not how it was called.

EDIT: Here's the context of the use. I'm using Mike Bostock's reusable chart pattern and this instance includes a method for getting/setting a label for a donut.

To me, this API usage follows the principle of least astonishment, as it's how I would expect the result to be returned.

var donut = APP.rotatingDonut();

// set label for one element
d3.select('#donut1.donut')
    .call(donut.label, 'Donut 1')

d3.select('#donut2.donut')
    .call(donut.label, 'Donut 2')

// set label for multiple elements
d3.selectAll('.donut.group-1')
    .call(donut.label, 'Group 1 Donuts')


// get label for one donut
var donutOneLabel = d3.select('#donut1').call(donut.label)
// donutOnelabel === 'Donut 1'

// get label for multiple donuts
var donutLables = d3.selectAll('.donut').call(donut.label)
// donutLabels === ['Donut 1', 'Donut 2', 'Group 1 Donuts', 'Group 1 Donuts']

and the internal method definition:

App.rotatingDonut = function() {

  var label = d3.local();

  function donut() {}

  donut.label = function(context, value) {
    var returnArray;
    var isList = context._groups[0] instanceof NodeList;

    if (typeof value === 'undefined' ) {

      // getter
      returnArray = context.nodes()
          .map(function (node) {return label.get(node);});

      return isList ? returnArray : returnArray[0];
    }

    // settter
    context.each(function() {label.set(this, value);});

    // allows method chaining
    return donut;
  };

  return donut
}
  • Very nice and interesting question. I wrote an answer which is not the answer you expect. If someone else proves me wrong, I'll happily delete it. – Gerardo Furtado Aug 15 '17 at 02:08
  • Just a curiosity: *why* are you doing this? Why don't simply pass a second argument to the function, informing the type of method? You see, this is a strong candidate for being a [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) – Gerardo Furtado Aug 15 '17 at 02:08
  • I've updated my post to give full context. The goal here is to find the "most d3-like" way of implementing this getter/setter method. The "best" answer would be an example of d3 doing this same thing for one of it's methods. – Ryan Lee Simms Aug 15 '17 at 17:05

1 Answers1

2

Well, sometimes a question here at S.O. simply doesn't have an answer (it has happened before).

That seems to be the case of this question of yours: "Is there a more built in way of determining if a selection comes from select or selectAll?". Probably no.

To prove that, let's see the source code for d3.select and d3.selectAll (important: those are not selection.select and selection.selectAll, which are very different from each other).

First, d3.select:

export default function(selector) {
    return typeof selector === "string"
        ? new Selection([[document.querySelector(selector)]], [document.documentElement])
        : new Selection([[selector]], root);
}

Now, d3.selectAll:

export default function(selector) {
    return typeof selector === "string"
        ? new Selection([document.querySelectorAll(selector)], [document.documentElement])
        : new Selection([selector == null ? [] : selector], root);
}

As you can see, we have only two differences here:

  1. d3.selectAll accepts null. That will not help you.
  2. d3.selectAll uses querySelectorAll, while d3.select uses querySelector.

That second difference is the only one that suits you, as you know by now, since querySelectorAll:

Returns a list of the elements within the document (using depth-first pre-order traversal of the document's nodes) that match the specified group of selectors. The object returned is a NodeList. (emphasis mine)

And querySelector only...:

Returns the first Element within the document that matches the specified selector, or group of selectors.

Therefore, the undocumented (and hacky, since you are using _groups, which is not a good idea) selection._groups[0] instanceof NodeList you are using right now seems to be the only way to tell a selection created by d3.select from a selection created by d3.selectAll.

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171