kjo
kjo

Reputation: 35311

How to create "svg" object without appending it?

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?

Upvotes: 38

Views: 16542

Answers (6)

altocumulus
altocumulus

Reputation: 21578

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.

Upvotes: 16

Sagi
Sagi

Reputation: 9284

I am working with version 4.4.4

var svg = document.createElementNS(d3.namespaces.svg, "svg");

Upvotes: 1

brichins
brichins

Reputation: 4065

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.

Upvotes: 3

user879121
user879121

Reputation:

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();
  }
}

Upvotes: 8

Drew Noakes
Drew Noakes

Reputation: 310907

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');

Upvotes: 31

Paul
Paul

Reputation: 1220

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

var svg = document.createElementNS(d3.ns.prefix.svg, 'svg');

Upvotes: 24

Related Questions