Hypergardens
Hypergardens

Reputation: 151

Creating SVG elements without rendering them

How can I create a function that returns a custom SVG graphic without appending it to anything? Can I create an empty selection and return that? What I currently have:

function makeGraphic(svgParent) {
    return svgParent.append('circle').attrs({...});
}

This is what I want:

function makeGraphic() {
    return d3.makeCircle?.attrs({...});
}

svgParent.append(makeGraphic());

Upvotes: 3

Views: 931

Answers (2)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

It seems to me that this is the perfect situation for using a detached element. You could also use DocumentFragment, but a detached element is more idiomatic in a D3 code.

For creating the detached whatever SVG element all you need is:

var detached = d3.create('svg:whatever');

According to the documentation,

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

This is very similar to Andrew's answer, because internally d3.create uses document.createElement. But pay attention, it uses document.createElement, not document.createElementNS, so the namespace is necessary here:

var detached = d3.create('svg:whatever');
//this is the namespace----ˆ

Then, after that, you can create the structure you want. For instance:

detached.selectAll(null)
    .data(d3.range(5))
    .enter()
    .append("circle")
    //etc...

Finally,get the node of the detached element and append it wherever you want, using append:

svg.append(function(){ return detached.node();});

Here is a demo, the function makeGraphic creates a detached element, and returns its node. Then, we just append the returned value to the SVG:

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

function makeGraphic() {
  var detached = d3.create('svg:g');
  detached.selectAll(null)
    .data(d3.range(10))
    .enter()
    .append("circle")
    .attr("cx", function() {
      return Math.random() * 500
    })
    .attr("cy", function() {
      return Math.random() * 300
    })
    .attr("r", function() {
      return Math.random() * 50
    })
    .attr("fill", function(_, i) {
      return d3.schemeCategory10[i]
    });
  return detached.node();
};

svg.append(makeGraphic)
<script src="https://d3js.org/d3.v5.min.js"></script>

Upvotes: 2

Andrew Reid
Andrew Reid

Reputation: 38151

You could create a temporary svg node with svg namespace inside the function - without rendering the svg. Then you can treat the svg as a typical node while you manipulate it and return some sort of shape.

One thing to note is that .append() accepts a tag name or a function, so I provide the function not its result below:

var svg = d3.select("body")
  .append("svg")
  .attr("width", 400)
  .attr("height",200)
  .attr("transform", "translate(200,100)");
  
var makeCircle = function() {
  // create a temporary svg
  let svg = document.createElementNS(d3.namespaces.svg, "svg")
  // create a circle
  let circle = d3.select(svg).append("circle")
    .attr("r", 20)
    .attr("fill", "steelblue");
    
  // return a circle
  return circle.node(); 

}

svg.append(makeCircle);
<script src="https://d3js.org/d3.v5.min.js"></script>

With that you could make a more complicated shape generator to return shapes for use with things like d3.append():

let svg = d3.select("body").append("svg")
    .attr("width", 500)
    .attr("height", 200);
    

var shapes = [{shape: "circle",y: 40, fill: "darkblue"},{shape:"square", y: 35},{y:40}]

svg.selectAll()
  .data(shapes)
  .enter()
  .append(shapemaker);    

function shapemaker(options = {}) {
	 let svg = document.createElementNS(d3.namespaces.svg, "svg")
	 var shape;
	 if (options.shape == "circle") {
	   shape = d3.select(svg).append("circle")
       .attr("cx", options.x ? options.x : 50)
       .attr("cy", options.y ? options.y : 50) 
       .attr("r", options.r ? options.r : 10)
       .attr("fill", options.fill ? options.fill : "steelblue" )
    }
    else if (options.shape == "square") {
      shape = d3.select(svg).append("rect")
       .attr("x", options.x ? options.x : 100)
       .attr("y", options.y ? options.y : 50) 
       .attr("width", options.width ? options.size : 10)
       .attr("height", options.width ? options.size : 10)
       .attr("fill", options.fill ? options.fill : "orange" )		
    }
    else {
      let x = options.x ? options.x : 150, y = options.y ? options.y : 50;
      shape = d3.select(svg).append("path")
        .attr("d", d3.symbol().type(d3.symbolStar).size(options.size ? options.size : 100))
        .attr("transform","translate("+[x,y]+")")
        .attr("fill", options.fill ? options.fill : "crimson")
		
    }
	 
  return shape.node();
	   
}
<script src="https://d3js.org/d3.v5.min.js"></script>

Upvotes: 2

Related Questions