38

Consider the following code:

var svg = d3.select('#somediv').append("svg").attr("width", w).attr("height", h);

I would like to refactor this code so that it reads more like this:

var svg = makesvg(w, h);
d3.select("#somediv").append(svg);

Note that, in contrast to the situation shown in the first version, in this second version append does not create the "svg" object; it only appends it to d3.select("#somediv").

The problem is how to implement the function makesvg. This in turn reduces to the problem: how to instantiate an "svg" object without using append to do this, since one could then do something like:

function makesvg(width, height) {
  return _makesvg().attr("width", w).attr("height", h);
}

So my question boils down to what is the generic equivalent of the hypothetical _makesvg() factory mentioned above?

Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
kjo
  • 33,683
  • 52
  • 148
  • 265
  • It is certainly possible to create a node without inserting it into the DOM but not with D3. Can I ask what you are trying to achieve, though? Knowing why you want to do this might help with a better answer. – Scott Cameron Aug 27 '13 at 00:59
  • @ScottCameron - I also want the same. I want a component which generates DOM (in this case D3 SVG element) and returns that object. it is up to the developer to append the result to the page, and not the component itself, therefor I need it to generate it's own SVG element and return it (after tons of D3 had been applied) – vsync Mar 15 '16 at 17:24

6 Answers6

31

You can use the following:

var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');

Note the use of createElementNS. This is required because svg elements are not in the same XHTML namespace as most HTML elements.

This code creates a new svg element, as you would regardless of using D3 or not, and then creates a selection over that single element.

This can be made marginally more succinct but clearer and less error prone as:

var svg = document.createElementNS(d3.ns.prefix.svg, 'svg');
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • 1
    I'm trying to accomplish this as well. With `var svg = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))`, I get `Failed to execute 'createElementNS' on 'Document': The qualified name provided ('[object SVGSVGElement]') contains the invalid name-start character '['.` Any ideas? I'm on chrome, but this should run pretty much anywhere, of course. – ElRudi Aug 20 '14 at 23:21
  • @ElRudi, you should really ask another question, not comment here. I suspect you're passing an SVG element to the function, not a string. – Drew Noakes Aug 21 '14 at 11:17
  • For d3 v4, use `var svg = document.createElementNS(d3.namespace("svg:text").space, "svg")` or the original statement – Charles L. Jul 15 '18 at 19:53
24

To save a little bit of time you can use d3.ns.prefix.svg

var svg = document.createElementNS(d3.ns.prefix.svg, 'svg');
Ionică Bizău
  • 109,027
  • 88
  • 289
  • 474
Paul
  • 1,190
  • 3
  • 12
  • 24
  • 2
    This didn't work for me using D3 v4 `TypeError: d3.ns is undefined`. Using the accepted answer's first choice worked flawlessly though. – jonlink Jan 30 '18 at 16:12
  • Update: it appears you'd now accomplish the same (or similar) thing by using `document.createElementNS(d3.namespace("svg:text"), 'svg')` *however* it results in a malformed SVG in cursory my testing. – jonlink Jan 30 '18 at 16:21
15

Finally, with the release of D3 v5 (March 22nd, 2018) this can now be done in D3 itself. This does not affect the other answers, whatsoever, which are still valid and in the end, D3 is going to use document.createElementNS() like described in previous posts.

As of v5 you can now use:

# d3.create(name) <>

Given the specified element name, returns a single-element selection containing a detached element of the given name in the current document.

This new feature can be used as follows:

// Create detached <svg> element.
const detachedSVG = d3.create("svg");

// Manipulate detached element.
detachedSVG
  .attr("width", 400)
  .attr("height", 200);

// Bind data. Append sub-elements (also not attached to DOM).
detachedSVG.selectAll(null)
  .data([50, 100])
  .enter()
  //...

 // Attach element to DOM.
 d3.select("body")
   .append(() => detachedSVG.node());

Have a look at the following snippet for a working demo creating a detached sub-tree, which is attached on a click event:

// Create detached <svg> element.
const detachedSVG = d3.create("svg");

// Manipulate detached element.
detachedSVG
  .attr("width", 400)
  .attr("height", 200);

// Bind data. Attach sub-elements.
detachedSVG.selectAll(null)
  .data([50, 100])
  .enter().append("circle")
    .attr("r", 20)
    .attr("cx", d => d)
    .attr("cy", 50);

// Still detached. Attach on click.
d3.select(document)
  .on("click", () => {
     // Attach element to DOM.
     d3.select("body")
       .append(() => detachedSVG.node());
  });
<script src="https://d3js.org/d3.v5.js"></script>

The main advantages of this approach are ease of use and clarity. The creation of the element is done by D3 behind the scenes and it is made available as a full-fledged selection with all its methods at hand. Apart from manipulating the newly created detached element, the returned selection can easily be used for data-binding.

It is also worth noting, that the function is not restriced to create elements from the SVG namespace but can be used to create an element from any namespace registered with D3.

altocumulus
  • 21,179
  • 13
  • 61
  • 84
  • the problem with d3.create is that it requires a globsal "document" object which doesn't exist if you trying to render an SVG on a node.js service ( i.e. without using a browser ). is that any simple workaroudn? – kroe Jun 05 '18 at 14:56
8

Here's an example function that creates an unattached group element:

function createSomething(){
  return function(){
    var group = d3.select(document.createElementNS(d3.ns.prefix.svg, 'g'));
    // Add stuff...
    return group.node();
  }
}

You can call it like so:

node.append(createSomething());

Explanation

Let's say you are rendering a collapsible tree and you want to have plus/minus icons with a circle border as the toggles. Your draw function is already enormous so you want the code for drawing the plus sign in it's own function. The draw/update method will take care of proper positioning.

One option is to pass the existing container into a function:

createPlus(node).attr({
  x: 10,
  y: 10
});

function createPlus(node){
  var group = node.append('g');
  // Add stuff...
  return group;
}

We can make this better by applying the technique from @Drew and @Paul for creating unattached elements.

node.append(createPlus())
    .attr({
      x: 10,
      y: 10
    });

function createPlus(){
  var group = d3.select(document.createElementNS(d3.ns.prefix.svg, 'g'));
  // Add stuff...
  return group;
}

Except that throws an error because append() expects either a string or a function.

The name may be specified either as a constant string or as a function that returns the DOM element to append.

So we just change it to:

node.append(function(){
  return createPlus();
});

But that still doesn't work. It causes the following error:

TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.

Luckily I found selection.node() which does work! Though, admittedly, I have no idea why.

function createPlus(){
  var group = d3.select(document.createElementNS(d3.ns.prefix.svg, 'g'));
  // Add stuff...
  return group.node();
}

We can save us a little more time by moving the anonymous function into createPlus:

node.append(createPlus())

function createPlus(){
  return function(){
    var group = d3.select(document.createElementNS(d3.ns.prefix.svg, 'g'));
    // Add stuff...
    return group.node();
  }
}
  • This is the key point: "...append() expects either a string or a function." d3.create() creates a D3 selection object, which is neither a string nor function. Hence why the anonymous function is needed - it 'converts' the detached element (from d3.create()) into a form that's usable by d3.append(). – xax Jul 02 '21 at 00:02
3

D3 author Mike Bostock suggests another (simpler) approach in his comment on an old D3 Github "issue" asking about this very topic:

Another strategy you could consider is to remove the element from the DOM immediately after creating it:

var svg = d3.select("body").append("svg")
    .remove()
    .attr("width", w)
    .attr("height", w);

svg.append("circle")
    .attr("r", 200);

document.body.appendChild(svg.node());

This approach does indeed append the element on creation, but .removes it immediately prior to manipulation and creation of child elements, so there should be no browser repaint. While technically contrary to the original question, this is probably the most idiomatic way to meet the requirement.

brichins
  • 3,825
  • 2
  • 39
  • 60
1

I am working with version 4.4.4

var svg = document.createElementNS(d3.namespaces.svg, "svg");
Sagi
  • 8,972
  • 3
  • 33
  • 41