Selecting individual elements in d3 - javascript

I've used the following code to create several rectangles and place them in a horizontal line:
var nodeIcons = svg.selectAll(".node")
.data(line.nodes)
.enter().append("rect")
.attr("x", screenWidth+100);
lastX = 50;
nodeIcons.transition().attr("x", function(d){
lastX += 150;
return lastX;
}).duration(1000);
As you can see, the rectangles are initially placed at the same x coordinate off the left edge of the screen, then they are animated into their place on the line.
Now, what would I call if I wanted to move just the first node in the line a bit further to the left?
I'm trying to wrap my head around the fundamentals of d3, here, and what I'm primarily asking is how to select the first node on the line.

First of all you probably want to give the class that you've selected the elements by to the created elements:
var nodeIcons = svg.selectAll(".node")
.data(line.nodes)
.enter().append("rect")
.attr("class", "node")
.attr("x", screenWidth+100);
To select just the first of these, you could either use .selectAll() and then index into the selection as pointed out in the comments, or simply use svg.select(".node"), which will give you the first element that matches.
D3 uses CSS selectors and these are quite flexible when it comes to things like this. If you wanted to select the 4th element for example, you could do something like this:
svg.select("rect:nth-child(4)");
In general, a better way to do this is to assign IDs to the elements so that you can select them explicitly though:
svg.append("rect").attr("id", "foo");
svg.select("#foo");

Related

How can I make x divs rather than a single div of x width using d3?

I am attempting to present some data vaguely similarly to what is done in https://xkcd.com/980, though on a much smaller scale.
What I am attempting to do is separate each of the horizontal bars (the divs created in the enter() and append()) into a series of smaller blocks, according to the average of the total that they make up (d/d3.sum(data)*100).
I am using the d3 library, but I haven't seen an example that has attached code nor that does quite the same thing. My own attempts have either failed or are, to my knowledge of JavaScript and d3, not possible.
I've adapted a simple bar chart to be labeled by percentage rather than count, here.
d3.select(".chart")
.selectAll("div")
.data(data)
.enter().append("div")
//can't enter and manipulate via loop?
.style("width", function(d) { return x(d) + "px"; })
//or perhaps, using %age, use that to loop and make div?
.text(function(d) { return Math.round(d/d3.sum(data)*100); });
I've also tried entering the div once it is created after assigning text, using d3's selectAll("div") followed by using the number in the array (.data(data[d])) and then entering and appending, but have been met with no luck.
Within the creation of the bars, where would I put a line creating inner div tags and how can I make sure that the correct amount are nested inside?

d3.js size node based on label text

Im have a force directed graph with a bunch of various nodes. Each node has either a single word, or double word label.
I have the svg circle and text label inside a group element. I plan on styling the label so it overlaps the node. My issue is that the majority of the time, the text will overflow the node.
Is there anyway to change the radius of the node based on its label size?
Let's say that nodes contains your g groups, that you've already bound data to them, and that the label text string is in a property name. You probably used this string when you told d3 to add the text elements to the g groups. You can use the same bound data to configure the circles when you add them. Something like this should work:
nodes.append("circle")
.attr("r", function(d) {return d.name.length * 2.5;})
... more stuff here.
The second line is the key. I'm just setting the circle radius based on the length of the label text. I used 2.5 as the multiplier based on trial and error with the default san-serif in 10pt type.
In theory, it would be nice to have some systematic method for determining how much each character takes up, on average, and use that to determine the multiplier in the second line. (Even with fixed-width fonts, there's a lot of variation in how much space is used for different fonts with the same point size.) If it were me, that would be more work than it was worth. I would probably just set a variable containing the multiplier near the top of the script and try to remember to change it when I changed fonts.
EDIT: It might be possible to use one of the functions getBBox() or getBoundingClientRect() on the text object (probably referencing it as this) to figure out the size of the text.
Try using getComputedTextLength().
From Mike Bostock:
http://bl.ocks.org/mbostock/1846692
node.append("text")
.text(function(d) { return d.name; })
.style("font-size", function(d) { return Math.min(2 * d.r, (2 * d.r - 8) / this.getComputedTextLength() * 24) + "px"; })
This allows you to fill up the circle with text without overflowing. I'm not totally sure where the numbers come from there—perhaps someone can better explain.
Alternatively, you could use getBBox() as in this example (and the other answer by Mars), though you'd need to also do some calculations for the circle. You can do that using .attr("text-anchor", "middle") and some geometry.
Hope this helps.

Gradient along links in D3 Sankey diagram

Here is jsfiddle of a Sankey diagram:
I am trying to modify colors of the links so that the color of each link is actually gradient from its source node color to its target node color. (it is assumed that opacity will remain 0.2 or 0.5 depending whether a mouse hovers or not over the link; so links will remain a little "paler" than nodes)
I took a look at this nice and instructive example, which draws this gradient filled loop:
However, I simply couldn't integrate that solution to mine, it looks too complex for the given task.
Also, note that links in original Sankey diagram move while node is being dragged, and must display gradient even in those transitory states. A slight problem is also transparency of links and nodes, and order of drawing. I would appreciate ideas, hints.
#VividD: Just saw your comment, but I was about done anyway. Feel free to ignore this until you've figured it out on the own, but I wanted to make sure I knew how to do it, too. Plus, it's a really common question, so good to have for reference.
How to get a gradient positioned along a line
With the caveat for anyone reading this later, that it will only work because the paths are almost straight lines, so a linear gradient will look half-decent -- setting a path stroke to a gradient does not make the gradient curve with the path!
In initialization, create a <defs> (definitions) element in the SVG and save the selection to a variable:
var defs = svg.append("defs");
Define a function that will create a unique id for your gradient from a link data object. It's also a good idea to give a name to the function for determining node colour:
function getGradID(d){return "linkGrad-" + d.source.name + d.target.name;}
function nodeColor(d) { return d.color = color(d.name.replace(/ .*/, ""));}
Create a selection of <linearGradient> objects within <defs> and join it to your link data, then set the stop offsets and line coordinates according to the source and target data objects.
For your example, it probably will look fine if you just make all the gradients horizontal. Since that's conveniently the default I thought all we would have to do is tell the gradient to fit to the size of the path it is painting:
var grads = defs.selectAll("linearGradient")
.data(graph.links, getLinkID);
grads.enter().append("linearGradient")
.attr("id", getGradID)
.attr("gradientUnits", "objectBoundingBox"); //stretch to fit
grads.html("") //erase any existing <stop> elements on update
.append("stop")
.attr("offset", "0%")
.attr("stop-color", function(d){
return nodeColor( (d.source.x <= d.target.x)? d.source: d.target)
});
grads.append("stop")
.attr("offset", "100%")
.attr("stop-color", function(d){
return nodeColor( (d.source.x > d.target.x)? d.source: d.target)
});
Unfortunately, when the path is a completely straight line, its bounding box doesn't exist (no matter how wide the stroke width), and the net result is the gradient doesn't get painted.
So I had to switch to the more general pattern, in which the gradient is positioned and angled along the line between source and target:
grads.enter().append("linearGradient")
.attr("id", getGradID)
.attr("gradientUnits", "userSpaceOnUse");
grads.attr("x1", function(d){return d.source.x;})
.attr("y1", function(d){return d.source.y;})
.attr("x2", function(d){return d.target.x;})
.attr("y2", function(d){return d.target.y;});
/* and the stops set as before */
Of course, now that the gradient is defined based on the coordinate system instead of based on the length of the path, you have to update those coordinates whenever a node moves, so I had to wrap those positioning statements in a function that I could call in the dragmove() function.
Finally, when creating your link paths, set their fill to be a CSS url() function referencing the corresponding unique gradient id derived from the data (using the pre-defined utility function):
link.style("stroke", function(d){
return "url(#" + getGradID(d) + ")";
})
And Voila!

Adding a title to my SVG window erases one datapoint? (d3)

I have a dataset of 100 numbers, and within an SVG I create a bunch of text objects to display those numbers using the code below:
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
console.log(output_format(d));
return output_format(d);
This works perfectly. However, if I try to create a title later on (outside of my d3.csv brackets) with this code:
svg.append("text")
.text("Actual Labels")
.attr("x", w/1.92)
.attr("y", top_gap/1.5)
.attr("class", "title");
Then the first datapoint gets erased, and does not even display in console.log(output_format(d));. What is happening here and how do I fix this?
What happens is that your single text element is appended first because the other code has to wait for the AJAX request. So when you're appending the remainder of your text elements, one is already there. This existing text element is selected by selectAll("text") and then matched with the data in dataset. By default, d3 matches data based on the index -- the first element in the array matches the first element that is already there and is therefore not in the .enter() selection which you operate on.
The easiest way to fix this is to give the text labels that you append dynamically a different class and select based on that. That is, your code for appending the dynamic labels would look like
svg.selectAll("text.label")
.data(dataset)
.enter()
.append("text")
.attr("class", "label")
.text(function(d) {
console.log(output_format(d));
return output_format(d);
});
No other changes should be required.

Cannot make labels work on zoomable d3 sunburst

After fiddling around for several hours now, I still cannot make labels work in my D3 Sunburst layout. Here's how it looks like:
http://codepen.io/anon/pen/BcqFu
I tried several approaches I could find online, here's a list of examples I tried with, unfortunately all failed for me:
[cannot post link list because of new users restriction]
and of course the coffee flavour wheel: http://www.jasondavies.com/coffee-wheel/
At the moment i fill the slices with a title tag, only to have it displayed when hovering over the element. For that I'm using this code:
vis.selectAll("path")
.append("title")
.text(function(d) { return d.Batch; });
Is there something similar I could use to show the Batch number in the slice?
--
More info: Jason Davies uses this code to select and add text nodes:
var text = vis.selectAll("text").data(nodes);
var textEnter = text.enter().append("text")
.style(...)
...
While the selection for his wheel gives back 97 results (equaling the amount of path tags) I only get one and therefore am only able to display one label (the one in the middle)
Needs some finessing but the essential piece to get you started is:
var labels = vis.selectAll("text.label")
.data(partition.nodes)
.enter().append("text")
.attr("class", "label")
.style("fill", "black")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.text(function(d, i) { return d.Batch;} );
You can see it here
The trick is that in addition to making sure you are attaching text nodes to the appropriate data you also have to tell them where to go (the transform attribute with the handy centroid function of the arc computer).
Note that I do not need vis.data([json]) because the svg element already has the data attached (when you append the paths), but I still have to associate the text selection with the nodes from each partition.
Also I class the text elements so that they will not get confused with any other text elements you may want to add in the future.

Categories