New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
.attr( map ) and .attr( function() {return map} ) #179
Conversation
See related discussions in #55 and #65. The key comment: "That suggests we could support taking a map for So, yeah. I'm onboard with this pull request, and I'd be happy to include it in the next release. I think we should extend this syntax to |
Sounds great. I'm quite interested in the function form too, as I think it could simplify code that needs to switch between orientations. |
Agree |
…assed in as the first argument, similar to jQuery or Raphael
This change has been applied to You can check out the new syntax in chord.js I interpreted the "function form" you both mentioned as passing in anonymous functions as values in the map, but delaying their execution until mapped to data. That should be the current behavior. Is this correct? Optional execution of function values was not implemented. A self-executing function, There is a new function, |
No, the inverse of that, which is "a function that returns a map". For example: .attr(function() { return {rand: Math.random()}; }) It's possible to support both, though I'm a little worried about the complexity and performance. But being similar to jQuery is also valuable. I'll take a look at your implementation and see if I have any suggestions. Thanks! |
Added basic support for the function form The issue is .attr( function(d) {
return {
"cx": function() { return 100 + Math.random() * 800; },
"cy": function() { return 100 + Math.random() * 800; },
"r": 50 * d,
"fill": "hsl(" + (100*d+100) + ", 70%, 50%)"
}
}); Find the above code in action on hello sort. |
Nice work! I think it's probably overkill to support functions that contain functions, although I appreciate your attention to detail and desire to support consistent behavior. We started with two types of input: 1- A constant, such as 42. Initially, I was thinking of adding two more types, in the single-argument form of 3- A constant map, such as This latter form should be sufficient for the single-argument form, where you might even want to control dynamically which attributes you specify. Now, I agree there are other possible forms:
While these might be expected, for consistency's sake, they don't seem particularly useful given that they can always be expressed by the fourth form above. For the sake of keeping the implementation small and fast, I'd prefer to support as few forms as possible. I'm normally in favor of strict parsimony, so for me the fourth form is the really convincing use case, as it's the one that lets you alter which attributes you specify as a function of data. |
I agree that a function that a returns a map that contains functions isn't useful. My code snippet could be equivalently written: .attr( function(d) {
return {
"cx": 100 + Math.random() * 800,
"cy": 100 + Math.random() * 800,
"r": 50 * d,
"fill": "hsl(" + (100*d+100) + ", 70%, 50%)"
}
}); What about The rest is bikeshedding I disagree on a map that contains functions, which is cheap in complexity with a recursive call to It's a more straightforward translation from existing code, especially when only one attribute is a function of // chained attrs
.attr("y", y1)
.attr("height", function(d) { return y0(d) - y1(d); })
// map contains a function (the bikeshed)
.attr({
"y": y1,
"height": function(d) { return y0(d) - y1(d); }
});
// function returns a map
.attr(function(d) {
return {
"y": y1,
"height": y0(d) - y1(d)
};
}) TMTOWTDI. They all look nice to me. |
Yep, if you say attr({cx: null}), it should delete the "cx" attribute, as if you said attr("cx", null). If it's simpler to support a map that contains functions, then I say go for it. :) I'll take a look. |
@@ -1395,6 +1403,27 @@ function d3_selection(groups) { | |||
}; | |||
|
|||
groups.attr = function(name, value) { | |||
if (typeof name === "object") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This typeof check is a bit ambiguous. For example, today I can say selection.attr(new String("fill"), "white"), and the name argument is automatically coerced from an object to a string. This isn't a particularly strong counterexample, but I prefer to use the arguments.length check for method overloading as the behavior is more predictable.
Ah, wait. That won't work, because we already support a single-argument form for returning the value: selection.attr(name). So now I'm back to thinking we should have a separate attrs method that takes a map so as to avoid any ambiguity. I'm not sure whether it would be more or less confusing to have a different name for the method, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typeof
isn't ideal for detecting objects, especially since typeof null === 'object'
is true.
Though, jQuery uses this exact check in its access
helper function for overloading attr
, and I've never hit this issue in practice.
Assuming we have a method attrs(map), here's how it might work. First, define a method that handles the evaluation and application of the map function: function attrMapFunction() {
var x = map.apply(this, arguments), name, value;
for (name in x) {
value = x[name];
name = d3.ns.qualify(name);
value == null
? (name.local ? this.removeAttributeNS(name.space, name.local) : this.removeAttribute(name))
: (name.local ? this.setAttribute(name, value) : this.setAttributeNS(name.space, name.local, value));
}
} Then, use a typeof check to branch evaluation for constants or functions: if (typeof map === "function") {
groups.each(attrMapFunction);
} else for (var name in map) {
groups.attr(name, map[name]);
}
return groups; I think that's pretty small and fast. What do you think? |
Oh, it might be cool if calling selection.attrs() returned a map of the defined attributes (mapping namespaces back to prefixes to match the input format)… More work, though. :) |
I'll give the return mapping of defined attributes a shot. I do strongly support overloading The function-which-returns-a-map form is a great addition though, since d3's |
My implementation branch is available in the map branch, with the main commit being @5603474872ac2cd9abe38b0eceac5e679b5a9080. I haven't tested the performance implications of this change, but it should be negligible. I decided not to implement the mode where If you're happy with my implementation, we could work on extending it to |
The implementation looks great! I'll take a look at how |
What's the status of this update? Has the |
Not yet. There's a lot more work to do to implement proper map support, including other operators. I've merged master into my map branch if you want to keep hacking on it. |
Great. This is the list of other operators required?
|
Yep. And probably a bunch of refactoring to avoid code duplication between the map and non-map based methods. |
Merged into #277. |
* Adopt type=module follow changes in d3-format: * type=module * add exports * remove zip * license: ISC * update dependencies * es6 rather than no-undef * use the "falls through" comment to make eslint's "no-fallthrough" rule happy * remove Sublime project * add eslint.json * stricter eslint * cleaner imports * update dependencies * Update README Co-authored-by: Mike Bostock <mbostock@gmail.com>
attr
can now take a map of name/value pairs, by calling itself recursively for each pair.attr
can now take a function which returns a map.