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>