d3 selector for immediate children

Viewed 45010

I can obviously do this:

d3.selectAll('div#some-div>ul')

But what if I'm using a DOM node or existing D3 selection:

d3.select(this).selectAll('ul')

will get me all descendent ULs. So, if

var div = d3.select('div') 

got me this node:

<div>
  <ul>
    <li>foo
      <ul><li>bar</li></ul>
    </li>
  <ul>
</div>

Then

var uls = div.selectAll('ul') 

will get me two ULs. I guess I could distinguish a top level one like:

uls.filter(function() { return this.parentNode === div.node() }

So, I've answered my own question. Maybe it will be useful to someone. Or maybe someone can recommend a less ugly solution.

Even better, Alain Dumesny, whose answer below is belatedly selected as correct, posted this as an issue to D3 and got the problem fixed, kludge-free, at the source! (I would copy it in here for convenience, but then people might not scroll down and cast greatly deserved upvotes for his heroic feat.)

8 Answers

The selectAll method relies on the querySelectorAll native method (in v4 at least).

It means you can use the :scope pseudo selector :

var uls = div.selectAll(':scope > ul') 

the :scope pseudo selector is currently a draft specification and is not supported in all browsers yet. More information on :scope pseudo selector available on MDN

Based on the solution by Sigfrid, here is something I added to the prototype, in a project I work on.

  /**
   * Helper that allows to select direct children.
   * See https://stackoverflow.com/questions/20569670/d3-selector-for-immediate-children
   *
   * @param {string} selector
   * @returns {Selection}
   */
  d3.selectAll('__nonexisting__').__proto__.MYPREFIX_selectChildren = function (selector) {
    var expectedParent = this.node();
    return this.selectAll(selector).filter(
      function() {
        return this.parentNode === expectedParent;
      }
    );
  };

The way that I grab the prototype object looks a bit clumsy. Perhaps there is a better way.

The "MYPREFIX_" is meant to prevent name clashes.

The jsdoc @returns {Selection} is ambiguous, unfortunately this type is declared within a closure and has no global name referenceable by jsdoc (afaik).

Once this file is included, you can then do this:

d3.select('#some_id').MYPREFIX_selectChildren('ul')

Looks like d3 used to have some functions built to address this exact problem- but for one reason or another they were removed. By pasting this code into your program, you can add them back in again:

function childMatcher(selector) {
  return function(node) {
    return node.matches(selector);
  };
}

function children() {
  return this.children;
}

function childrenFilter(match) {
  return function() {
    return Array.prototype.filter.call(this.children, match);
  };
}

/**
 * Runs the css selector only on the immediate children.
 * See: https://stackoverflow.com/questions/20569670/d3-selector-for-immediate-children
 * Use: https://github.com/d3/d3-selection/commit/04e9e758c80161ed6b7b951081a5d5785229a8e6
 *
 * Example Input: selectChildren("form")
 */
d3.selection.prototype.selectChildren = function(match) {
  return this.selectAll(match == null ? children
      : childrenFilter(typeof match === "function" ? match : childMatcher(match)));
}

function childFind(match) {
  return function() {
    return Array.prototype.find.call(this.children, match);
  };
}

function childFirst() {
  return this.firstElementChild;
}

/**
 * Runs the css selector only on the immediate children and returns only the first match.
 * See: https://stackoverflow.com/questions/20569670/d3-selector-for-immediate-children
 * Use: https://github.com/d3/d3-selection/commit/04e9e758c80161ed6b7b951081a5d5785229a8e6
 *
 * Example Input: selectChild("form")
 */
d3.selection.prototype.selectChild = function(match) {
  return this.select(match == null ? childFirst
      : childFind(typeof match === "function" ? match : childMatcher(match)));
}

If you are using typescript, then here is the function declaration you can include in the same file:

declare module "d3" {
    interface Selection<GElement extends d3.BaseType, Datum, PElement extends d3.BaseType, PDatum> {
        selectChild(match: string | null | Function): Selection<GElement, Datum, PElement, PDatum>;
        selectChildren(match: string | null | Function): Selection<GElement, Datum, PElement, PDatum>;
    }
}

Here's a fiddle that implements this: https://jsfiddle.net/Kade333/drw3k49j/12/

Related