How to create SVG elements of different types based on data?
I am trying to create a diverse legend, which has circles triangles, rectangles, lines etc, and I want to create these independently and then use d3 to arrange their position and coloring, but how would I access these data directly?
d3.selectAll('g.legend')
.data([
// is there a way to have d3 create an element in memory but not append it?
{ svgFn: function() { this.append('rect') }, ...otherinfo },
{ svgFn: function() { this.append('circle') }, ...otherinfo },
]).enter()
.append('g')
.append(function(d) { d.svgFn.call(this)})
.attr...
This question is a variation of the How to create elements based on data? or How to dynamically append elements? pattern. In my opinion your approach will be overly complex and cluttered because you need to duplicate functions to create elements in your data. This doesn't seem to be an elegant solution.
I would prefer specifying only the type of the element to create, i.e. {type: "circle"}
, {type: "rect"}
in your data objects, etc., and let the selection.append()
method do the working. This method will accept a callback which in turn may evaluate the type specifed in your data and create the elements accordingly:
# selection.append(type) <>
[...]
Otherwise, the type may be a function which is evaluated for each selected element, in order, being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element. This function should return an element to be appended.
This would simplify your code to just:
d3.selectAll('g.legend')
.data([
{ type: 'rect', other: info },
{ type: 'circle', other: info }
])
.enter().append('g')
.append(function(d) {
return document.createElementNS(d3.namespaces.svg, d.type);
});
Addendum
As requested by user2167582's comment the solution for assigning attributes can also be easily incorporated.
With D3 v4 using the d3-selection-multi module you may use the multi-value syntax passing in objects containing key-value pairs of attributes to set. Assuming your array of elements to be created to look like this:
var elementsAndAttributes = [
{ type: 'rect', attrs: { "fill": "blue", "width": "10", "height": "10" } },
{ type: 'circle', attrs: { "fill": "red", "cx": "20", "cy": "20", "r": "10" } }
];
You can then bind this data and create elements with their attributes in a single run:
d3.selectAll('g.legend')
.data(elementsAndAttributes)
.enter().append('g')
.append(function(d) { // Create elements from data
return document.createElementNS(d3.namespaces.svg, d.type); // v4 namespace
})
.attrs(function(d) { // Set the attributes object per element
return d.attrs;
});
When still using D3 v3 things are a bit different. Although v3 had the support for multi-value object configuration built-in, you were not allowed to provide the object as the return value of a function (see the issue #277 "Multi-value map support." for a discussion on why that was). You can, however, use selection.each()
to achieve the same thing.
d3.selectAll('g.legend')
.data(elementsAndAttributes)
.enter().append('g')
.append(function(d) { // Create elements from data
return document.createElementNS(d3.ns.prefix.svg, d.type); // v3 namespace
})
.each(function(d) { // Iterate over all appended elements
d3.select(this).attr(d.attrs); // Set the attributes object per element
});
Ignoring the differences in the way D3 references the namespace constants, this last version using selection.each()
will actually work in both D3 v3 as well as v4.
Further reading: My answer to "Object Oriented d3".
Based on the answer by altocumulus and reading the doc
selection.append("div");
Is equivalent to:
selection.append(d3.creator("div"));
I found a way to let d3 do the work and not be bothered with namespaces
var svg = d3.select("#chart"); // or any other selection to get svg element
d3.selectAll('g.legend')
.data(elementsAndAttributes)
.enter().append('g')
.append(function(d) { // Create elements from data
return d3.creator(d.type).bind(svg.node())();
})
.attrs(function(d) { // Set the attributes object per element
return d.attrs;
});
Be aware of the direct calling of the bound creator()
, because the result of the callback is used as the argument to appendChild(child)
. If bind
is unknown you can use
.append(function(d) { // Create elements from data
return d3.creator(d.type).call(svg.node());
})
The documentation speaks about that this
should be the parent node of where you want to append but that is not true, this
is only used to get this.ownerDocument
(=== document for browsers) and the result of the creator()
call is appended to the correct (g
) node. So any (svg) element is correct.