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>