D3 force layout text label overlapping - d3.js

I have a hard time positioning text label on my force chart. They are overlapping each other, and I could not figure out how to fix it. And I've tried many solutions from online, none of them works well. Could you please help me take a look?
Here are the code for my text labels:
var node_text = node_textNew.append("svg:text")
.attr("class", "text_note")
.attr("dx", 0)
.attr("dy", -0.5)
.attr('background-color', '#fff')
.attr("x", function(d, i) { return circleWidth + 5; })
.attr("y", function(d, i) { if (i>0) { return circleWidth + 0 } else { return 8 } })
.text(function(d) { return d.name});
Here is how it looks right now:
Thank you so much for your help!

Try this one http://bl.ocks.org/MoritzStefaner/1377729.
Here the author introduces a way when a label is placed near a node using another force layout.

One easy solution I have found us to use the center of a nodes Voronoi cell as the anchor for a label. This gives you the optimal spacing provided by your graph.
An example of this is seen in:
https://bl.ocks.org/mbostock/6909318

Related

D3 force layout: individually positioning first and last node

I would like to create a static force-directed layout as the one in this example: http://bl.ocks.org/mbostock/1667139, but with the first and last node anchored on each side of the window (contrary to the example, my graph is not circular). This way I could clearly see where the graph starts and ends and also hopefully be able to give it a more linear conformation.
So far I have attempted to assign different classes to these two nodes and then give those classes fixed coordinates, like:
svg.select(".first")
.attr("cx", function(d) { return 10; })
.attr("cy", function(d) { return height/2; })
svg.select(".last")
.attr("cx", function(d) { return width-10; })
.attr("cy", function(d) { return height/2; })
But it doesn't seem to be working, the two nodes are just sitting in the upper right corner of the window. Any ideas?
Placing the following code between the for loop and force.stop() seemed to work fine for me.
var nodes = force.nodes();
nodes[0].x = 10;
nodes[0].y = height/2;
nodes[nodes.length-1].x = width-10;
nodes[nodes.length-1].y = height/2;
Here's a Fiddle showing this. You might also want to look into the fixed property, which could help with the behaviour you're looking for.

Show labels conditionally on multi series line chart

In the d3 example of a multi series line chart, all the series labels are shown on the very right of the graph. What I'm interested in doing is changing that so they will conditionally show on certain parts of the chart, or not at all.
So using that example, if I wanted the labels to show at the max value on each series and be the same colour as each series (perhaps something like this), how do I go about achieving that? I can figure that it'll be something to do with the block
city.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; })
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
But I can't work it out. I've tried using the IndexOf or MaxOf features, but can't get either to work in that context. Aware this might cause some overlap on the graph, though I can deal with that later. Bonus points to anyone who can show how to add in conditional labels with lines that point to the max as well.
Thanks :)
without paying attention to the readability of the chart, one solution might be to first add a function:
function find_max_date(max,values)
{
for (i=0;i<values.length;i++)
{
if (values[i].temperature==max)
{
return i;
break;
}
}
}
and to use this code:
city.append("text")
.datum(function(d) {return {name: d.name, max: d3.max(d.values,function(d){return d.temperature}), max_date: d.values[find_max_date(d3.max(d.values,function(d){return d.temperature}),d.values)].date };})
.attr("transform", function(d) {return "translate(" + x(d.max_date) + "," + y(d.max) + ")"; })
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; }).attr("fill",function(d) {return color(d.name);});
so basically, this adds max and max_data to the data-object - max by simply using the d3.max-method to find the highest temperature, and max_data by looping through the original data per city and returning the position of the maximum temperature in the array, which is used to access the corresponding date. the color is added the same way as it is added to the lines.
edit: not sure about the "bonus"-question, what should the result look like? lines pointing at the maximum value for each city?
edit2: a possible solution for the bonus-question as i was curious if it was doable, but there might be better ways to do it. i changed different parts of the code, so you can delete the function find_max_date.
at the start of the script i added a global variable
var offset=20;
which serves as variable to control the distance of the labels between each other and the border. then, between the cities-color-domain-part and the scale-domains, i added the following code:
for (i=0;i<cities.length;i++)
{
cities[i].max=0;
cities[i].label_off=0;
for (j=0;j<cities[i].values.length;j++)
{
if (cities[i].values[j].temperature>cities[i].max)
{
cities[i].max=cities[i].values[j].temperature;
cities[i].max_date=cities[i].values[j].date;
}
}
}
this does basically the same as we did before with d3.max and the custom function to find the date of the maximum temperature, only this time i would suggest to add the values directly into the cities-variable. cities.label_off will be a value to put some space between labels which would otherwise be overlapping.
next, i added another loop to compare values on the x-scale to check if they overlap. simply put, if the distance between two x-values is smaller than 100 pixels, the label_off-value is set to the offset-value (in this case 20 pixel), otherwise the label_off-value remains at 0. you might want to adjust that, depending on how short/long your labels are.
for (i=0;i<cities.length;i++)
{
for (j=i+1;j<cities.length;j++)
{
if (Math.abs(x(cities[i].max_date)-x(cities[j].max_date))>0 && Math.abs(x(cities[i].max_date)-x(cities[j].max_date))<100) cities[j].label_off=offset;
}
}
next, i added the offset-variable to the y-domain, just to have a little more space to put the labels in place:
y.domain([
d3.min(cities, function(c) { return d3.min(c.values, function(v) { return v.temperature; }); }),
d3.max(cities, function(c) { return d3.max(c.values, function(v) { return v.temperature; })+offset; })
]);
and finally the part to add the labels and the lines. as the cities-variable already contains all the necessary information you don't need to use another datum and can just adress them using the property name:
city.append("text")
.attr("transform", function(d) {return "translate(" + (x(d.max_date)+30+d.label_off) + "," + (offset+d.label_off) + ")"; })
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; })
.attr("fill",function(d) {return color(d.name);});
city.append("line")
.attr("class","line_con")
.attr("x1",function(d){return x(d.max_date);})
.attr("y1",function(d){return y(d.max);})
.attr("x2",function(d){return (x(d.max_date)+30+d.label_off);})
.attr("y2",function(d){return (offset+d.label_off);})
.attr("stroke",function(d) {return color(d.name);});
as you can see, the transform just puts the labels on the x-scale at the position of the date of the maximum temperature. then it adds 30, which is an arbitrary value just to move them slightly to the right so they don't sit directly above the value. d.label_off adds 0 if there is no overlapping, and in our case 20 if there is.
on the y-scale i positioned them at the top of the chart with some space between them and the border using the offset. again, if there is overlapping, they are moved down a little bit.
for the lines, it's just connecting the maximum value and the starting point of the label. they use a different css-class to allow for the correspondning color:
.line_con {
fill: none;
stroke: none;
stroke-width: 1.5px;
again, there might be better ways to accomplish it and i'm not sure if this works fine on all sorts of data, but it might give you an idea how to handle it.

D3.js: Transferring the value of one attribute to another attribute, for a specific FDG node?

I'm using a force directed graph that appends circles to each node.
As part of the node creation, I first set the radius "r" of each node circle to a default and consistent value (defaultNodeSize = 10). This successfully draws a cluster where all related nodes are the same size.
// Append circles to Nodes
node.append("circle")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("r", function(d) { if (d.id==focalNodeID) { return centerNodeSize; } else { return defaultNodeSize; } } ) // <---------------------------- Radius "r" set HERE!!!!
.style("fill", "White") // Make the nodes hollow looking
.attr("type_value", function(d, i) { return d.type; })
.attr("color_value", function(d, i) { return color_hash[d.type]; })
.attr("rSize", function(d, i) { return d.rSize; }) // <------------------ rSize HERE!!!!
.attr("id", "NODE" )
.attr("class", function(d, i) {
var str = d.type;
var strippedString = str.replace(/ /g, "_")
//return "nodeCircle-" + strippedString; })
if (d.id==focalNodeID) { return "focalNodeCircle"; }
else { return "nodeCircle-" + strippedString; }
})
.style("stroke-width", 5) // Give the node strokes some thickness
.style("stroke", function(d, i) { return color_hash[d.type]; } ) // Node stroke colors
.call(force.drag);
Also, upon creation, I set an attribute called "rSize", which specifies that node's absolute magnitude. Each node has a different rSize and rSize is different than the defaultNodeSize. The purpose of rSize is so that I can access it, later, and dynamically change the circle's radius from it's defaultNodeSize to it's rSize (or the reverse) allowing each node to expand or contract, based on controllers.
In a separate controller function, I later select all nodes I want to apply the new rSize to. Selecting them is easy...
var selectedNodeCircles = d3.selectAll("#NODE");
However, I don't know what the syntax is to read each node's rSize and apply rSize to that specific node that's being changed. I would think that it's something like...
selectedNodeCircles.("r", function(){ return this.attr("rSize"); });
In other word's, I'd like to retrieve that specific node's "rSize" attribute value and set the attribute "r" to the value retrieved from "rSize".
Any idea of what the correct syntax is to do this?
Thanks for any help you can offer!
You are looking for the getAttribute() function.
So something like this should work for you:
selectedNodeCircles.attr("r", function() {return this.getAttribute("rSize")})
Remember that this in the function, is the circle itself and hence simply an element in the DOM, to the best of my understanding.
You can confirm this by simply printing out this using console.log(this) right before the result statement.
Hope this helps.

How do you get the width of a shape that's already created in d3?

cells.append('rect')
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
.attr('height', function (d) { return d.dy; })
.attr('fill', function (d) { return d.children ? null : color(d.parent.name); })
cells.append('text')
.attr('x', function (d) { return d.x + d.dx / 2 })
.attr('y', function (d) { return d.y + d.dy / 2 })
.attr('fill', 'white')
.attr('font-size', 14)
.text(function (d) {
if (d.name.length < cells.rect.width) {
return d.name;
}
});
I don't know why
cells.rect.width
doesn't work? But what I want to know is how I get check the width of the shape before appending the text on to the shape.
I also tried pushing the width of the shapes to an array, and then use indexing loop to loop through the array to check to see if the length of the name is longer than the width of the box. The problem with that is when the shapes are created, the shape(box) with the biggest area are created first. On the other hand, when text are appended, it starts from bottom right to top left.
I understand that the length of the text and pixels are two different units, but I am going to put in a simple ratio convert it. How do I get the width of the rect that is already drawn on the screen?
You can use the element (this) and its bbox to get the actual width. This will not work as you want it to though, because you are comparing the number of characters with the width in pixels. To make it work, you need to draw the text first, then check its width compared to the rect and remove it if it is too large.
The code for this (after having drawn rects and text) would be something like this.
cells.selectAll("text")
.text(function(d, i) {
if(this.getBBox().width <
document.querySelectorAll("svg > rect")[i].getBBox.width) {
return d.name;
}
});
The rect object has an attributes child inside it.
cells.rect.attributes.width.value
might work instead.

Parenting D3.js-Nodes for dragging

I have a network of nodes and links. One of them has a fixed position in the center but is draggable, the others are in a force field around the centered one. If the user drags any node, the others will be draged behind him, because the are linked. Is there a possibility to drag the others with the centered node, but keeping the drag-event of the other nodes single?
thanks for thinking about it,
David
edit: if someone knew a possibility to set a dragg-listener for all the other nodes to the centered one, the problem would be solved. I'd be grateful if you had an idea!
Please leave me a comment which parts of th ecode could help you solve this issue, and I'll post it asap!
edit: with the help of nrabinowitz I can now move the nodes just as I wanted! But the new code-parts somehow crashed my coordinate-restrictions. For the nodes not to drop out of the svg, I put a cx/cy-attr to all nodes, preventing them from crossing the border of svg. This still works in the beginning, but after the first drag of the center-node (and therefore the 'g'-element) the restrictions seem to shift. Is there anything dragged except the svg?
The part of the script providing the restriction is
force.on("tick", function() {
node.attr("cx", function(d) { return d.x = Math.max(15, Math.min(width - 15, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(15, Math.min(height - 15, d.y)); });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
link.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; });
});
See working fiddle: http://jsfiddle.net/nrabinowitz/4Rj4z/
This encloses the nodes in a g element, and uses the transform attribute to move them around.
In order to get this to work, you can't use force.drag for the node you want to have pull the group - you need a custom d3.behavior.drag. Unfortunately, per this answer, you can't put the drag handler on the node itself - it needs to be on the group. This works, but it means that other elements without a separate drag handler - e.g. links - will also drag the group. You might be able to fix this with pointer-events on the group.

Resources