How to create SVG elements of different types based on data?

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”.

Leave a Comment