Nested SVG node creation in d3.js - javascript

I'v just started playing with d3js and find it strange that I have to create multiple selectors for each element I want to link to the background data structure for example separate selectors such as one for overlay text and one for rectangles to make an annotated bar graph.
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr('y',function(d,i){return i*10;})
.attr('height',10)
.attr('width',function(d){return d.interestingValue})
.fill('#00ff00');
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr('y',function(d,i){return i*10;})
.fill('#0000ff')
.text(function(d){return d.interestingValue});
Is there a more convenient way of combining these into a single selection and enter() chain that creates both the rects and the text elements?

Use a G (group) element. Use a single data-join to create the G elements, and then append your rect and text element. For example:
var g = svg.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(0," + i * 10 + ")"; });
g.append("rect")
.attr("height", 10)
.attr("width", function(d) { return d.interestingValue; });
g.append("text")
.text(function(d) { return d.interestingValue; });

Related

D3 append text to circles on a line not working properly

I created some line charts on d3. After that I appended some circles to every 3rd element in the line. Now I want to append text to those circles.
// created a circle variable
var circles=g2
.append("g")
.attr("id","symbols-b")
.selectAll("circles")
.data(slicesCircle)
.enter()
.append("g")
circles.style("fill", function(d){
return d.color=color(d.id);
})
//append circles
circles
.selectAll("circle")
.data(function (d){return d.values})
.enter()
.filter((d,i)=>(i%3==0) && i>0) //to attach every 3rd datapoint
.append("circle")
.attr("r", 7.5)
.attr("cx", function(d,i) {return xScale(d.date);})
.attr("cy", function(d,i) {return yScale(d.measurement);})
// Using the circle variable again to append text
circles
.selectAll("circle")
.data(function (d){return d.values})
.enter()
.filter((d,i)=>(i%3==0) && i>0)
.append("text")
.attr("x",function(d,i) {return xScale(d.date);})
.attr("y",function(d,i) {return yScale(d.measurement);})
.text("0") // testing with 0
.style("stroke","white")
.style("font-size","12px")
But when I am running the code, the last 2 blocks despite being the same are generating different outputs. The circles are being generated nicely but when I am appending text to the same block of code, it is beginning later. Unable to understand why so..
Changed .selectAll("circle") to .selelctAll("text") to select each text placeholder rather than circle. Worked
// Using the circle variable again to append text
circles
.selectAll("circle")
.data(function (d){return d.values})
.enter()
.filter((d,i)=>(i%3==0) && i>0)
.append("text")
.attr("x",function(d,i) {return xScale(d.date);})
.attr("y",function(d,i) {return yScale(d.measurement);})
.text("0") // testing with 0
.style("stroke","white")
.style("font-size","12px")
// Using the circle variable again to append text
circles
.selectAll("text")
.data(function (d){return d.values})
.enter()
.filter((d,i)=>(i%3==0) && i>0)
.append("text")
.attr("x",function(d,i) {return xScale(d.date);})
.attr("y",function(d,i) {return yScale(d.measurement);})
.text("0") // testing with 0
.style("stroke","white")
.style("font-size","12px")

dot (symbol) color on d3.js multiline chart

I am trying to replicate this example of a multiline chart with dots. My data is basically the same, where I have an object with name and values in the first level, and then a couple of values in the second level inside values. For the most part, my code works, but for some reason, the j index in the anonymous function for the fill returns an array of repeated circle instead of returning the parent of the current element. I believe this may have something to do with the way I created the svg and selected the elements, but I can't figure it out. Below is an excerpt of my code that shows how I created the svg, the line path and the circles.
var svgb = d3.select("body")
.append("svg")
.attr("id","svg-b")
.attr("width", width)
.attr("height", height)
var gameb = svgb.selectAll(".gameb")
.data(games)
.enter()
.append("g")
.attr("class", "gameb");
gameb.append("path")
.attr("class", "line")
.attr("d", function(d) {return line_count(d.values); })
.style("stroke", function(d) { return color(d.name); })
.style("fill", "none");
gameb.selectAll("circle")
.data(function(d) {return d.values;})
.enter()
.append("circle")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y_count(d.count);})
.attr("r", 3)
.style("fill", function(d,i,j) {console.log(j)
return color(games[j].name);});
j (or more accurately, the third parameter) will always be the nodes in the selection (the array of circles here), not the parent. If you want the parent datum you can use:
.attr("fill", function() {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
return color(datum.name);
})
Note that using ()=> instead of function() will change the this context and the above will not work.
However, rather than coloring each circle independently, you could use a or the parent g to color the circles too:
gameb.append("g")
.style("fill", function(d) { return color(d.name); })
.selectAll("circle")
.data(function(d) {return d.values;})
.enter()
.append("circle")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y_count(d.count);})
.attr("r", 3);
Here we add an intermediate g (though we could use the original parent with a few additional modifications), apply a fill color to it, and then the parent g will color the children circles for us. The datum is passed on to this new g behind the scenes.

How to append multiple items to a force simulation node?

I'm using D3 v4 and can't seem to get multiple items to append to a node. In the code below I'm trying to get text to appear with the image as part of my force simulation. Both the image and text need to move together around the screen. It works perfectly if I only append either the image or the text but I can't get it to group both. When I run this it just shows 1 node in the corner.
this.node = this.d3Graph.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(Nodes)
.enter()
.append("svg:image")
.attr("xlink:href", 'https://seeklogo.com/images/T/twitter-2012-negative-logo-5C6C1F1521-seeklogo.com.png')
.attr("height", 50)
.attr("width", 50)
.append("text")
.attr("x", 20)
.attr("y", 20)
.attr("fill", "black")
.text("test text");
this.force.on('tick', this.tickActions);
tickActions() {
this.node
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
this.force
.restart()
}
You cannot append a <text> element to an <image> element. You have to append the <text> to the <g>.
The easiest solution is breaking your selection:
this.node = this.d3Graph.selectAll(null)
.data(Nodes)
.enter()
.append("g")
.attr("class", "nodes");
this.node.append("svg:image")
.attr("xlink:href", 'https://seeklogo.com/images/T/twitter-2012-negative-logo-5C6C1F1521-seeklogo.com.png')
.attr("height", 50)
.attr("width", 50);
this.node.append("text")
.attr("x", 20)
.attr("y", 20)
.attr("fill", "black")
.text("test text");
Here we use the data to create <g> elements in the enter selection. Then, to each <g> element, we append an <image> and a <text> as children.

doesn't update d3.js elements in .each function

My code works and it shows me the elements for my data, but d3 doesn't update the text of my SVG Element after changing my data and running the same code again. I have to refresh the whole site for it to change.
var blackbox= d3.select("#content")
.selectAll(".silencer")
.data([0]);
blackbox.enter()
.append("div")
.attr("class", "silencer")
.attr("id", "silencer");
blackbox.exit().remove();
var box = blackbox.selectAll(".ursa")
.data(fraung);
box.enter()
.append("div")
.attr("class", "ursa")
.each(function(d) {
d3.select(this)
.append("svg")
.attr("class", "invoker")
.each(function(d) {
d3.select(this).append("svg:image")
.attr("xlink:href", "images/qwer.png")
.attr("x", "0")
.attr("y", "0")
.attr("width", "100")
.attr("height", "100")
.append("title")
.text(function(d) {return (d.name)});
});
d3.select(this).append("div")
.attr("class", "chen")
.each(function(d) {
d3.select(this).append("table")
.attr("class", "tab")
.each(function(d) {
d3.select(this).append("tbody")
.each(function(d) {
d3.select(this).append("tr")
.text(function(d) {return("Name: ")})
.attr("class", "key")
.each(function(d) {
d3.select(this).append("td")
.text(function(d) {return (d.name)});
});
});
});
});
});
box.exit().remove();
It sounds like your SVG element isn't being cleared before you load the second data set and so the second data set is being drawn behind the first. If the only thing that is changing is the text, it would look like nothing is happening at all. A browser refresh would clear any dynamically drawn content. Before you invoke "blackbox", you can do something like this to clear everything in the "#content" element, which I'm assuming is your SVG container.
if(!d3.select("#content").empty()) d3.select("#content").remove();
This will remove the SVG container entirely. So you'll have to create it again before you can load in new data. Alternatively if you just want to remove the child elements from the SVG container, you could do this:
if(!d3.select("#content").selectAll("*").empty()) d3.select("#content").selectAll("*").remove();

Placing labels at the center of nodes in d3.js

I am starting with d3.js, and am trying to create a row of nodes each of which contains a centered number label.
I am able to produce the desired result visually, but the way I did it is hardly optimal as it involves hard-coding the x-y coordinates for each text element. Below is the code:
var svg_w = 800;
var svg_h = 400;
var svg = d3.select("body")
.append("svg")
.attr("width", svg_w)
.attr("weight", svg_h);
var dataset = [];
for (var i = 0; i < 6; i++) {
var datum = 10 + Math.round(Math.random() * 20);
dataset.push(datum);
}
var nodes = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d, i) {
return (i * 70) + 50;
})
.attr("cy", svg_h / 2)
.attr("r", 20);
var labels = svg.append("g")
.attr("class", "labels")
.selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr("dx", function(d, i) {
return (i * 70) + 42
})
.attr("dy", svg_h / 2 + 5)
.text(function(d) {
return d;
});
The node class is custom CSS class I've defined separately for the circle elements, whereas classes nodes and labels are not explicitly defined and they are borrowed from this answer.
As seen, the positioning of each text label is hard-coded so that it appears at the center of the each node. Obviously, this is not the right solution.
My question is that how should I correctly associate each text label with each node circle dynamically so that if the positioning of a label changes along with that of a circle automatically. Conceptual explanation is extremely welcome with code example.
The text-anchor attribute works as expected on an svg element created by D3. However, you need to append the text and the circle into a common g element to ensure that the text and the circle are centered with one another.
To do this, you can change your nodes variable to:
var nodes = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(dataset)
.enter()
// Add one g element for each data node here.
.append("g")
// Position the g element like the circle element used to be.
.attr("transform", function(d, i) {
// Set d.x and d.y here so that other elements can use it. d is
// expected to be an object here.
d.x = i * 70 + 50,
d.y = svg_h / 2;
return "translate(" + d.x + "," + d.y + ")";
});
Note that the dataset is now a list of objects so that d.y and d.x can be used instead of just a list of strings.
Then, replace your circle and text append code with the following:
// Add a circle element to the previously added g element.
nodes.append("circle")
.attr("class", "node")
.attr("r", 20);
// Add a text element to the previously added g element.
nodes.append("text")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
Now, instead of changing the position of the circle you change the position of the g element which moves both the circle and the text.
Here is a JSFiddle showing centered text on circles.
If you want to have your text be in a separate g element so that it always appears on top, then use the d.x and d.y values set in the first g element's creation to transform the text.
var text = svg.append("svg:g").selectAll("g")
.data(force.nodes())
.enter().append("svg:g");
text.append("svg:text")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
text.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
The best answer came from the asker himself:
just a further observation: with only .attr("text-anchor", "middle")
for each text element, the label is at the middle horizontally but
slightly off vertically. I fixed this by adding attr("y", ".3em")
(borrowed from examples at d3.js website), which seems to work well
even for arbitrary size of node circle. However, what exactly this
additional attribute does eludes my understanding. Sure, it does
something to the y-coordinate of each text element, but why .3em in
particular? It seems almost magical to me...
Just add .attr("text-anchor", "middle") to each text element.
Example:
node.append("text")
.attr("x", 0)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
This page describes what's going on under the svg hood when it comes to text elements. Understanding the underlying machinery and data structures helped me get a better handle on how I had to modify my code to get it working.

Categories

Resources