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.
Related
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
I have this working code. where the d3 part is basically:
var bar = chart.append("div").attr("class", "chart")
.selectAll('div')
.data(scope.data.sort().reverse()).enter().append("div")
.transition().ease("elastic")
.style("width", function(d) { return (d[0]/sum)*attrs.chartWidth + "px"; })//This is where I base the width as a precentage from the sum and calculate it according to the chart-width attribute
.style("background-color",function(){i++;if (i<=colors.length-1){return colors[i-1]} else {return colors[(i-1)%colors.length]}}).text(function(d) { return d[1] ; })
but when I try to append("span") in the chaining so the text would be on the span and not in the parent div. the text just disappears and the dev console shows no clue of both the span and the text. Also tried insert("span") and even replacing the .text for .html(function(d){return "<span>"+d[1]+"</span>"}
neither work.
any clues? Thanks!
The problem is that you are starting a transition in the chain. The transition object provides many functions just like a normal d3.selection including .remove, .text and .html, but does not allow .append operation.
You should re-factor the code to read:
var bar = chart.append("div").attr("class", "chart")
.selectAll('div')
.data(scope.data.sort().reverse()).enter().append("div");
bar
.transition().ease("elastic")
.style("width", function(d) { return (d[0]/sum)*attrs.chartWidth + "px"; })//This is where I base the width as a precentage from the sum and calculate it according to the chart-width attribute
.style("background-color",function(){i++;if (i<=colors.length-1){return colors[i-1]} else {return colors[(i-1)%colors.length]}}) })
bar.append('span')
.text(function(d) { return d[1] });
Demo
As a side note, while selecting the background-color, you do not need to maintain the index variable yourself, d3 passes the data d and the index i to the setter function you provide to .style:
.style("background-color",
function(d, i){ // <-- 'd' and 'i' are passed by d3
if (i<=colors.length-1)
{return colors[i-1]}
else {return colors[(i-1)%colors.length]}})
})
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.
In this code, http://enjalot.com/inlet/4124664/
of which the main part is:
function render(data) {
var nodz = svg.selectAll(".node")
.data(data);
nodz.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return "id"+d[0]+d[1]; })
.attr("x",0)
.attr("y", 0)
.attr("transform", function(d) {
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
})
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("stroke", "black")
.text(function(d) {return d[2]; });
// update
nodz.selectAll("text")
.text(function(d) {
return d[2]; });
// another go
d3.selectAll("text")
.text(function(d) {
return d[2]; });
}
// data in form [x pos, y pos, value to display]
var nodes = [[0,0,0],[1,1,0],[1, -1,0],[2,0,0], [2,2,0]];
render(nodes);
nodes2 = [[0,0,1],[1,1,1],[1, -1,1],[2,0,1], [2,2,1], [2, -2,1]];
render(nodes2);
I call the code to draw some nodes twice.
I expect it to draw five nodes with a value of zero in the first pass,
Then I add another item to the list and update all the values to 1 so expect to see all the values change to 1 and a new node appear. Instead, I'm only seeing the last one being set to 1. I've tried adding a unique id to bind the node to the data but this isn't working. Also tried reselecting to see if the data is now bound. In all the tutorials I've been through, just calling the selectAll().data() part updates the data, what am I missing?
The second optional argument to .data() is a function that tells d3 how to match elements. That's where you need to compare your IDs, see the documentation. That said, it should work without IDs in your case as it matches by index by default.
The problem with updating the text is that after calling .selectAll() you need to call .data() again to let d3 know what you want to match to that selection (i.e. that the new data should be bound to the old data).
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.