I have data objects that I want to add to an SVG. Consider the following pseudo-snippet:
var data = [], counter = 0;
for (var col=1; col<=5; col++)
for (var row=1; row<=3; row++)
data.push({
id: "obj-" + ++counter
,x: col * 120
,y: row * 120
,width: 40
,height: 40
,shape: counter % 2 ? "circle" : "rect"
});
d3.select(".container").selectAll(".obj")
.data(data)
.enter()
.append("g")
.attr("id", function(d){ return d.id; }
/***
now I want to draw here a circle or rect based on the shape key
so if (d.shape == "rect") -- we will use width and height
if (d.shape == "rect" && d.width == d.height) we will set "r" to "width", etc.
***/
Ideally, I would create an object of type Shape, e.g.
function Shape(id, shape, x, y, w, h) {
this.id = id;
this.shape = shape;
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.render = function(parent) {
var g = parent.append("g")
.attr("id", this.id);
switch (this.shape) {
case "circle":
g.append("circle")
.attr( /* more code here */ )
break;
case "rect":
g.append("rect")
.attr( /* more code here */ )
break;
case "triangle":
g.append("polygon")
.attr( /* more code here */ )
break;
}
}
}
Then I'd be able to do something like:
var data = [], counter = 0;
for (var col=1; col<=5; col++)
for (var row=1; row<=3; row++)
data.push(new Shape({
id: "obj-" + ++counter
,x: col * 120
,y: row * 120
,width: 40
,height: 40
,shape: counter % 2 ? "circle" : "rect"
)});
But how can I call the Shape's render() method from d3? i.e.
d3.select(".container").selectAll(".obj")
.data(data)
.enter()
/* given a datum named d, call d.render(parent) ? */
I'm rather new to d3, so maybe data joins is the wrong way to go? Is there a different way to render data items that would be better for this scenario?
To have the data objects render themselves in an object-oriented manner you can resort to a less known use of selection.append(name)
. According to the docs, you can provide a callback to .append()
which needs to return a DOM element to append:
selection.append(name)
[…]
The name may be specified either as a constant string or as a function that returns the DOM element to append. If name is a function, it is passed the current datum d and the current index i, with the this context as the current DOM element. To append an arbitrary element based on the bound data it must be created in the function. For example:
selection.enter().append(function(d) {
return document.createElementNS("http://www.w3.org/2000/svg", d.type)
})
For the purpose of this question this may be modified to not create the element in place, but to delegate the creation to the .render()
method of your data objects.
d3.select(".container").selectAll("g")
.data(data)
.enter()
.append(function(d) {
return d.render(); // .render() will return the DOM element to append
});
Your .render()
method might look something like this:
this.render = function() {
// Create the group.
var g = document.createElementNS(d3.ns.prefix.svg, "g");
g.setAttribute("id", this.id);
// Create and configure the child element based on this.shape.
var child;
switch (this.shape) {
case "circle":
child = document.createElementNS(d3.ns.prefix.svg, "circle");
child.setAttribute("cx", this.x);
child.setAttribute("cy", this.y);
child.setAttribute("r", this.width/2);
break;
case "rect":
child = document.createElementNS(d3.ns.prefix.svg, "rect")
break;
case "triangle":
child = document.createElementNS(d3.ns.prefix.svg, "polygon")
break;
}
// Append the child to the group and return the g DOM element.
g.appendChild(child);
return g;
}
Have a look at this snippet for a working example:
function Shape(id, shape, x, y, w, h) {
this.id = id;
this.shape = shape;
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.render = function() {
// Create the group.
var g = document.createElementNS(d3.ns.prefix.svg, "g");
g.setAttribute("id", this.id);
// Create and configure the child element based on this.shape.
var child;
switch (this.shape) {
case "circle":
child = document.createElementNS(d3.ns.prefix.svg, "circle");
child.setAttribute("cx", this.x);
child.setAttribute("cy", this.y);
child.setAttribute("r", this.width/2);
break;
case "rect":
child = document.createElementNS(d3.ns.prefix.svg, "rect")
child.setAttribute("x", this.x);
child.setAttribute("y", this.y);
child.setAttribute("width", this.width);
child.setAttribute("height", this.height);
break;
case "triangle":
child = document.createElementNS(d3.ns.prefix.svg, "polygon")
break;
}
// Append the child to the group and return the g DOM element.
g.appendChild(child);
return g;
}
}
var data = [], counter = 0;
for (var col=1; col<=5; col++)
for (var row=1; row<=3; row++)
data.push(new Shape(
"obj-" + ++counter
,counter % 2 ? "circle" : "rect"
,col * 120
,row * 120
,40
,40
));
console.log(data);
d3.select(".container").selectAll("g")
.data(data)
.enter()
.append(function(d) {
return d.render(); // .render() will return the DOM element to append
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="400" height="400">
<g class="container"></g>
</svg>
I've been interested in this topic as well. Here's what I'm currently doing. In your case, instead of setting text to d.text, you would choose the shape you want. Or maybe someone might enlighten us both on a better way.
// file grid.js
var chipView = Chip().radius(config.chip.radius);
function updateView() {
view.chips = view.renderLayer.selectAll('.draggable-chip')
.data(model.chips /* , id */)
.call(chipView, 'update')
;
view.chips.enter()
.append('g')
.call(chipView)
.classed('chip draggable-chip', true)
.call(d3.behavior.drag()
.on('dragstart', chipDragStarting)
.on('drag', chipDragging)
.on('dragend', chipDragEnding)
)
;
view.chips
.attr('transform', function(d,i){return translateString(d.col*config.grid.size, d.row*config.grid.size);})
;
}
// file chip.js
var Chip = function() {
var config = {
r: 20
};
function chipView(g, updateOnly) {
// g is an array of groups (a d3 selection)
// each g will become a chip
updateOnly = (typeof updateOnly === 'string' || updateOnly === true);
g.each(function() {
var view = d3.select(this); // the svg group in g array
function initView() {
// clear any existing svg nodes inside 'g'
var contents = view.selectAll(this.childNodes);
if (contents)
contents.remove();
// draw the chip
view.append('circle')
.attr('r', config.r)
.attr('cx', 0)
.attr('cy', 0)
;
view.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '0.33em')
.text(function(d,i){return d.text;})
;
}
if (!updateOnly)
initView();
function updateView() {
view.attr('opacity', function(d,i){return 1 - 0.5 * d.ghost;});
view.select('text').attr('opacity', function(d,i){return 1 - d.ghost;});
}
updateView();
});
}
chipView.radius = function(r) {
if (!arguments.length)
return config.r;
r = parseFloat(r);
if (typeof r !== 'number')
throw 'Error: radius must be a number.';
config.r = r;
return this; // for function chaining
};
return chipView;
};