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
Selection append support for naked nodes and functions #732
Conversation
I'd like to see more discussion about how this would be used. |
I've implemented this because it was needed at work: We're building a generic component that takes an unknown container (can be a list, or a div, etc) for which we define a "prototype" renderer in markup. This renderer can be an arbitrary chunk of html that has been styled, etc.. Our component pulls that node from the component and caches it. When we join the data we append a new clone of the for each of the data items and we then update each with the datum. A simplified example would go along these lines. Given the markup, <html>
...
<li class="prototype"> <!-- or ".template", etc. -->
<span class="price">Price</span>
::
<span class="index">Index</span>
<p>... other stuff</p>
</li> We do something along the lines of ;requirejs.config({ shim: { 'd3': {exports: 'd3'}} })
require(["d3", "jquery"], function(d3, $) {
var $prototype = $(".prototype").remove(), format = d3.format("+3.3f")
ul = d3.select("body").append("ul")
li = ul.selectAll("*").data(d3.range(10).map(format))
li.enter().append(function(d, i) { return $prototype.clone().get(0) })
li.select(".price").text(function(d, i) { return d; })
li.select(".index").text(function(d, i) { return i; })
}); We "managed" to implement this without appending the raw node by appending stub "span" nodes, then on an |
... We're also expecting to be able to select among various data renderers based on the data type... We wish to have certain markup structures that can render certain kinds of articles, for example, and some other configuration for other kinds of research, etc. and be able to decide at the moment of appending the nodes. Hopefully from some markup chunk written (and styled) as html and not have to reconstruct them using javscript as they can get pretty complex. We're using d3 to build all our data-driven components. Not only charts but mixed activity feeds for publications, selectors, typeaheads which mix text and images, etc. |
... and even if just to be able to do things like, div.enter().append(function(d, i) { return Math.random() > 0.5 ? $("<p>something" + d + "</p>").get(0) : $("<h2>other" + i + "</h2>").get(0) }) and get <div><p>something+0.000</p><p>something+1.000</p><p>something+2.000</p><h2>other3</h2><h2>other4</h2><p>something+5.000</p><p>something+6.000</p><p>something+7.000</p><h2>other8</h2><p>something+9.000</p><p>something+10.000</p><h2>other11</h2><p>something+12.000</p><h2>other13</h2><p>something+14.000</p><p>something+15.000</p><p>something+16.000</p><p>something+17.000</p><p>something+18.000</p><p>something+19.000</p><h2>other20</h2></div> :^) |
Seems like selection.clone(node) would be a closer fit to what you want, so as to support templating. But even then, you might want selection.appendClone and selection.insertClone, or at least allow an optional argument to selection.clone(node, before) (essentially making clone equivalent to insertClone). I'm hesitant to allow selection.append(function), since there's no convenient way to use it—your function gets called multiple times, so you need to carefully return the correct node each time the function is called. A selection.clone(node) method is safer since it can create new nodes for you automatically. Though there's still the awkwardness that you need to specify a reference to the node to clone (perhaps Another possibility might be selection.clone(selector). For example, say you had some HTML: <ul>
<li class="template">
<span class="price"></span>
<span class="index"></span>
</li>
</ul> You might say: var item = d3.select("ul").selectAll(".item").data(items).enter().clone(".template");
item.select(".price").text(…);
item.select(".index").text(…); So, the node matching the selector ".template" is first removed from the DOM, and then clones are appended for each element in the enter selection. Here's how you could extend the enter prototype to do that: d3.selection.enter.prototype.clone = function(selector) {
var template, parent;
return this.select(function() {
if (parent !== this) template = d3.select(parent = this).select(selector).remove().node();
return this.appendChild(template.cloneNode(true));
});
}; This isn't perfect because the original selector ".item" doesn't match the template selector ".template"; you'd want to remove the "template" class and add the "item" class. It'd be better to use ".item" as the selector for both, but then the first datum will match the template node and end up in the update selection rather than the enter selection, throwing the whole thing off. I suppose selectAll(".item:not(.template)") would work, but that's fairly awkward (and all the cloned nodes will still have the class "template"). Note that you can pretty much do this already using enter.select: var item = d3.select("ul").selectAll(".item").data(items).enter().select(clone(".template"));
item.select(".price").text(…);
item.select(".index").text(…);
function clone(template) {
template = d3.select(template).remove().node();
return function() {
return this.appendChild(template.cloneNode(true));
};
} Here ".template" is found globally, though the better solution (as in selection.clone) would only select within the each group's parentNode, i.e., within the previously-selected UL element. |
Thanks for the reply. And I agree, I understand that in order to use With the patch the current functionality of We do have one (for us very important) use case which couldn't be fulfilled by the clone method. This is being able to decide which node to return based on the datum item. It is in this context that I find the function argument quite convenient: enter.append(function(d, i) {
return d.type == "publication" ? publicationRenderer.cloneNode(true)
: d.type == "research" ? researchRenderer.cloneNode(true)
: "p" // default?
}); ... as illustrated on this very toyish example (which also shows how the nodes can be easily plucked and potentially prepared for rendering): https://gist.github.com/3215903#file_index.html I like the manual selection / attachment you suggest on your last example and we can use this approach on our project. But I don't see how that's more intuitive than just allowing the function as argument for append / insert. |
To me, there's several issues being surfaced by this: ConsistencyPractically everything else supports functions, I'm guessing that most people would be genuinely surprised that Similarly, while Node Confusion@mbostock It seems like your primary concern here is around inserting raw nodes (and screwups from re-inserting existing nodes). What about:
or:
It is, however, tremendously useful to be able to synthesize nodes via whatever framework you're comfortable using (say, a Backbone view) - you also avoid having to create wrapper elements or modify the DOM multiple times... DOM MutationsDOM insertions are pretty heavy (and you especially feel the pain on a mobile device), cutting them to a minimum has been a great performance boon for my projects so far. I think we can get to a pretty comfortable world where those are cut down to a reasonable level while also producing clear & concise D3 code. For example, the main thing I'm using (via #734) is to pass a function for the |
You’re correct that many methods in D3 accept both constants and functions are arguments. When a function is accepted, its return value is typically the same type as the accepted constant. For example, with selection.attr(name, value), the value can be specified as a string or a function that returns a string (computing the value from data). Note, however, that the attribute name cannot be specified as a function. This would be possible to implement, but I think there is a practical limit to what should be defined as a function. While restrictive, it is rare that you would need to compute the attribute name dynamically, as commonly only attribute values need to be computed from data. It is for the same reason that selection.append and selection.insert accept only strings rather than functions that return strings; it is rare that you need to compute the element name dynamically. That selection.select and selection.selectAll also accept a function is somewhat of a special-case to allow extensibility. I would guess that most people don’t know that these methods accept functions, since they are almost always used with selector strings; so, I disagree that most people are genuinely surprised that selection.append and selection.insert do not accept functions. Strictly speaking, if you wanted selection.select and selection.selectAll to be consistent with other function usage in D3, the input function should return a string (the selector string computed dynamically from data) rather than a node or an array of nodes. But this would make them significantly less powerful for extension; for example, you could no longer select via XPath functions. You could allow both types of return values and use type inference to determine whether the function returned a string or a node, but that introduces complexity and a performance cost; furthermore, I don't see computing a selector dynamically especially useful. I definitely want to preserve selection.select and selection.selectAll accepting functions that return nodes because this provides a fantastic building block (or escape-hatch, if you will) for extensibility. So, should selection.append and selection.insert accept a function that returns a string? I would say no, for the same reason that the name passed to selection.attr need not be a function that returns a string. But should selection.append and selection.insert accept a function that returns a raw node? That seems more plausible to me—which is why there has been a long-standing TODO comment to this effect. Though I’m not totally convinced that such functionality is significantly more useful than passing a function to selection.select. And while you are correct that most people wouldn’t think of using selection.select to select elements that are created (or appended) dynamically, this seems like a power feature anyway, so I’m less worried about minimizing surprise. (Plus, it might be empowering to teach people that selection.append and selection.insert are thin wrappers on top of selection.select.) It seems moderately useful and consistent to allow the before argument for selection.insert to be a function which returns a node (probably using d3_selection_selector). I expect this has a performance cost for the common case where the before argument is a selector string, but it’s probably negligible. |
Awesome, thanks for the thorough response and rationale! I think I'm pretty well in line with your thinking now. For my specific problem, would a |
I also now agree... but only after having first done my own implementation of append for function that returns a raw node and after trying out the "select" direct way of creating and appending the node by hand. Trying both, one realizes that the functions you pass to either are very very similar, the only difference is the actual attaching the node you create before returning it on the select route. So now I know how little value the append(function--for raw node) provides. But before, just by going through the API and the documentation, that was not visible. If only for this, I wish there had been either a note on the selection.select documentation about this technique (which is not intuitive, I have to say) or the friendlier, thin, but welcomed sugar nicety of allowing the function that returns the node. |
If selection.append() supported functions, this sure would simplify code. Take this very simple case: a bar chart, where each bar is a filled "rect" and a "text" used as a tip for the datum, both sitting within a common parent "g". I want to benefit from the power of CSS, so I can create a CSS rule for when the user hover over the parent "g", the child "text" become visible (i.e.: "svg.barchart g.bar text.tip { display: none }" then "svg.barchart g.bar:hover text.tip { display: block }"). No javascript required. I can do that right now, but I need three consecutive select.append(), while in my opinion it would makes more sense to call select.append() once and let the user create whatever composite (or not) node he wants. Whenever the composite node grows in complexity, more append() calls are required. But then, I admit I am new to d3, so I might be overlooking the optimal way to do this. |
You can append bare nodes, but you have to pass a function to selection.select or selection.each, basically dropping down to the DOM API. For example: var node = document.createElement("span");
d3.select("body").select(function() { return this.appendChild(node); }); You could also use selection.each, of course: d3.select("body")
.each(function() { this.appendChild(document.createElement("h1")); })
.each(function() { this.appendChild(document.createElement("span")); }); One issue with append taking a function is whether this function should return a string (representing the name of the node to create, such as "span" or "g") or a node, or both. For selection.select(function), the function must return a node (not a selector string; that could be supported, but I think it would be rarely used). Also, selection.select(node) or selection.append(node) only works when the selection has a single element, so I don’t think this is appropriate to add to the API. |
The each() you propose is no different than what I see as a workaround now: multiple iterations are required in order to join data and composite DOM elements. Whether this function should return a string is really a non issue in my view, as you said somewhere above, that make no sense, this I agree. I do not understand your last sentence. I define a composite element as a single element, but with one or more children. So append() would be returned the single top element of the hierarchical ensemble. In the example I was giving, the function given to append() as an argument would instantiate and assemble The issue to me is that the current API to join data to DOM element is less friendly toward elements which are composite, which I believe is not an uncommon occurrence, as we are dealing here with objects which are inherently hierarchical, "g" or "div" elements are rather useless on their own. Lets have this CSS:
With current API:
While if append() accepted a function as an argument:
Obviously, this is a simple example, and the former form doesn't appear too much of a burden with this simple example. But I do believe supporting a function as an argument would remove a constraint from the current API which assume that whatever needs to represent a datum is a leaf DOM element. This new form of append() has also nice side effects
|
The append behavior you describe is nearly identical to selection.select(function). The only difference is that with selection.select, you also have to append the node yourself (not just create it). |
Ah I see. I tried it and I can indeed do the above using |
Yes, once one realizes that if you pass a function to One can then build all sorts of components to plug in there. At work we've built components that clone from other nodes or that build nodes from html snippets. But one can do many different things as well: selection functions that return objects from a pool, components that do subselections based on characteristics of the data or the nodes, components that return nodes that already live somewhere else on the DOM or even creating nodes that are not yet attached because we want to asynchronously build them before showing them... etc. For example, this great new library, https://github.com/sammyt/see , https://groups.google.com/forum/?fromgroups=#!topic/d3-js/dNemrm3UF1M which can make sure a certain DOM structure---described in a simple expression---is in place, building whatever elements are missing, and returning a new I think Mike is right in trying to keep the API small; it'd be hard to cater for all these different uses without overly complicating the calls. And it the most straightforward things one would like built are easy to express. |
Like selection.select, selection.append and selection.insert can now accept a function which returns a node. This makes it slightly easier to append or insert elements whose name is computed from data, or to append elements that already exist (say from an element pool). There has been much discussion regarding whether the function should return the name of the element or the element itself. Returning a name is less work for the caller, but only supports creating new elements; returning a name is also more consistent with how D3 defines attribute values, but D3 does not allow attribute names to be specified as functions. So, it seemed better to opt for consistency with selection.select and selection.selectAll, which accept functions that return elements, since this is more expressive. Of course, you can still use select and selectAll to append elements, but using append to do that directly is more intuitive. Related #4 #311 #724 #732 #734 #961 #1031 #1271.
Superseded by #1354. |
Hello Mike,
I'd like to collaborate selection-append support for naked dom nodes and for functions which, according to normal d3 style, will take the
(d, i)
data and index arguments and can return either a node or a qualified or unqualified name for a node.