1
var cs1 = drawCircles(svg1,data,d=>d["A"],d=>d["B"],sc1); 

Trying to get an intuitive understanding for how d is used when binding. The above function calls .append('circle') and uses the two access functions d=>d["A"],d=>d["B"] to place the circles.

I understand that d essentially acts as an iterable on 'data' and that here we are implicitly returning d['B']. What I don't understand is why we cant simply pass d in a non function way e.g. drawCircles(...,d['A'] ,...). When I've tried using d in this manner it returns d is not defined.

Using a simpler example,

svg.selectAll('circle').data(data).enter.append('circle')
  .attr('id', (d) => {return d['name']})

Why can't I simply supply d['name'] for the 2nd arg of .attr()

How can I reason about the use of d solely as a function argument?

Thanks!

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
Alex
  • 97
  • 5

1 Answers1

2

The 2nd Parameter of .style()/.attr()

When using something like:

.attr("fill", someParameter);

D3 checks to see if someParameter is a function or a constant.

If someParameter is a constant, "all elements are given the same attribute value", afterall we're using a constant. (see also, D3 documentation).

If someParameter is a function, D3 uses function.apply() (see also, MDN's function.apply() documentation) to call that function for each item in the selection, providing it with 3 parameters:

  • the current datum (by convention d),
  • the index of the current item (by convention i),
  • and the group of nodes in the selection (inconsistent convention, I'll use nodes here).

The use of apply also allows specification of this, which is the current element (which is also: nodes[i]).

The use of function.apply() defines d, i, and nodes only within the function provided.

This makes sense, if you provide a constant, there is nothing to apply, er, apply to and no need to.


What happens when you supply d['name'] as the 2nd argument for .style()/.attr()

If using:

.attr("fill", d.color)`

d in the above has no relation to the datum. If you haven't declared d and given it the property color yourself, it'll be undefined here. D3 doesn't call a function here with apply to define d - you aren't providing a function to do so with.

Only if d.color evaluates to function(d) { return d.color; } would you be able to do what you are asking about. This would be a very unusual form with D3.

If d is undefined you'll likely throw an error when accessing d.color as you've seen. If d is defined, but d.color isn't a function, it'll be treated as a constant and every element will gain a property with the same value.

Consequently, this is why we see the format:

.attr("fill", function(d,i,nodes) { return ... });

Stop Here

It is possible in theory, but not advisable, to accomplish what you think should be possible.

I'm only sharing because

  • I've had this lying around for a while
  • It shows how much of a workaround is required to achieve the pattern you are asking about (without pre-defining your functions as properties of some object named d).

Again it's not advisable to use - but you can, technically, dynamically create an accessor function for a given property, or nested property, of the datum with a proxy.

With this you could use the form:

.attr("fill", d.color) 

When accessing any of the proxy (d) properties (here color) it would need to return the appropriate accessor function (function(d) { return d.color;}) which would then be passed to .attr() and have the appropriate datum bound to it. You can only use the property, you wouldn't be able to use d.x + 2.

// create a proxy `d` to return accessor functions, 
var d = new Proxy({},{ get: f })

var data = [
  {style:{fill:"steelblue",stroke:{color:"crimson", width:4}},width: 30, y: 50, x: 10},
  {style:{fill:"yellow",stroke:{color:"orange", width:2}},width: 20, y: 50, x: 50},
  {style:{fill:"crimson",stroke:{color:"steelblue", width:8}},width: 30, y: 50, x: 80}
]

var svg = d3.select("body").append("svg");

svg.selectAll(null)
  .data(data)
  .enter()
  .append("rect")
  .attr("x", d.x)
  .attr("y", d.y)
  .attr("width", d.width)
  .attr("height",d.width)
  .attr("fill",d.style.fill)
  .attr("stroke-width",d.style.stroke.width)
  .attr("stroke", d.style.stroke.color);
  
  
  
// To resolve paths (https://stackoverflow.com/a/45322101/7106086):
function resolve(path, obj) {
    return path.reduce(function(prev, curr) {
        return prev ? prev[curr] : null
    }, obj || self)
}
// Proxy to dynamically access properties of any depth:
function f(obj, prop) {
    if(prop in obj) return obj[prop];
    else { 
       var g = function() {
          var accessor = function(d) {
            return resolve(accessor._path_,d);
          }
          // keep track of path:
          if(obj._path_) accessor._path_ = [...obj._path_,prop];
          else (accessor._path_) = [prop];
          return accessor;
       }
       return obj[prop] = new Proxy(g(), {get:f});     
    }
 }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • As this seems to be a beginner's question of sorts I am not sure if this is really helpful to OP. It will definitely earn you one of those invisible *Sickness* gold badges, though ;-) – altocumulus May 26 '20 at 09:34
  • 1
    @altocumulus, To be honest, it was more of a lark with you and Gerardo in mind. – Andrew Reid May 26 '20 at 16:53