D3.js Dynamic connector between objects - javascript

I'm very new to both JS and D3, and I've googled this a tonne but only found examples that are a bit too advanced.
I'm making a simple decision graph implementation, and I'm stuck trying to connect 2 nodes with a line / path. The objects can be moved around with the mouse, and the path should always update to reflect the positions of the objects.
This is my base source of knowledge: https://github.com/mbostock/d3/wiki/SVG-Shapes, but I don't quite understand how to do something smart with it.
Here is what I have so far: http://jsbin.com/AXEFERo/5/edit
Don't need the fancy stuff, just need to understand how to create connectors and have them update dynamically when the objects are being dragged around. Big thanks!

To draw a line between the circles, you don't need anything special -- just the line element.
var line = svg.append("line")
.style("stroke", "black")
.attr("x1", 150)
.attr("y1", 100)
.attr("x2", 250)
.attr("y2", 300);
Updating the position dynamically is a bit more difficult. At the moment, you have no means of distinguishing which of the circles is being dragged. One way of doing this is to add a distinguishing class to the g elements.
var g1 = svg.append("g")
.attr("transform", "translate(" + 150 + "," + 100 + ")")
.attr("class", "first")
...
and similarly for the other one. Now you can switch on the class in your dragmove function and update either the start or the end coordinates of the line.
if(d3.select(this).attr("class") == "first") {
line.attr("x1", x);
line.attr("y1", y);
} else {
line.attr("x2", x);
line.attr("y2", y);
}
Complete example here. There are other, more elegant ways of achieving this. In a real application, you would have data bound to the elements and could use that to distinguish between the different circles.

Related

How to connect two d3 js svg circles with a line or path? [duplicate]

I'm very new to both JS and D3, and I've googled this a tonne but only found examples that are a bit too advanced.
I'm making a simple decision graph implementation, and I'm stuck trying to connect 2 nodes with a line / path. The objects can be moved around with the mouse, and the path should always update to reflect the positions of the objects.
This is my base source of knowledge: https://github.com/mbostock/d3/wiki/SVG-Shapes, but I don't quite understand how to do something smart with it.
Here is what I have so far: http://jsbin.com/AXEFERo/5/edit
Don't need the fancy stuff, just need to understand how to create connectors and have them update dynamically when the objects are being dragged around. Big thanks!
To draw a line between the circles, you don't need anything special -- just the line element.
var line = svg.append("line")
.style("stroke", "black")
.attr("x1", 150)
.attr("y1", 100)
.attr("x2", 250)
.attr("y2", 300);
Updating the position dynamically is a bit more difficult. At the moment, you have no means of distinguishing which of the circles is being dragged. One way of doing this is to add a distinguishing class to the g elements.
var g1 = svg.append("g")
.attr("transform", "translate(" + 150 + "," + 100 + ")")
.attr("class", "first")
...
and similarly for the other one. Now you can switch on the class in your dragmove function and update either the start or the end coordinates of the line.
if(d3.select(this).attr("class") == "first") {
line.attr("x1", x);
line.attr("y1", y);
} else {
line.attr("x2", x);
line.attr("y2", y);
}
Complete example here. There are other, more elegant ways of achieving this. In a real application, you would have data bound to the elements and could use that to distinguish between the different circles.

Appending elements to d3 selection

In a d3 program, I have a many circle elements with data bound to them. In a callback to an event, I want to filter out most of them and place a larger red circle on top of those that are left. It looks something like this:
svg.selectAll("circle")
.filter(/* filter function not important */)
.append("circle")
.attr("class", "extra")
.attr("cx", function(){return d3.select(this).attr("cx")})
.attr("cy", function(){return d3.select(this).attr("cy")})
.attr("r", 5);
And the result is this (the first line is the original circle):
<circle cx="55.41208075590415" cy="279.3650793650794" r="1">
<circle class="extra" r="5"></circle>
</circle>
So I have two problems. One, it's inserting as a child of the old element, rather than a sibling. Ideally it would be inserted as the last child of svg. Second, cx and cy aren't getting copied over. What magic do I have to utter to make this work?
If you are okay overwriting existing elements, see the answer below. To create new ones, here's how.
svg.selectAll("circle")
.filter(/* filter function not important */)
.each(function(d){
svg.append("circle")
.attr("class", "extra")
.attr("cx", function() { return x(d.fop) })
.attr("cy", function() { return y(d.bar) })
.attr("r", 5);
})
Notice two things. One, the inner functions do not take parameters, because there is no data bound to the extra circle. Use the d from the existing circle instead. Two, I couldn't figure out how to access the cx and cy of the existing circle, so I had to recompute them instead, which means the scales x and y must remain unchanged. I'll change the accepted answer if anyone can improve on this.
Instead of doing a .append on your selection you should simply use your filter function to modify the circles whose data have the criterion that pass your filter function. You can modify those circles, instead of appending a new element. So your code would look something like this.
svg.selectAll('circle')
.filter(/** what you want**/)
.attr('r', newRadiusSize)
.attr('fill', 'red')
You could even add a nice transition to make the circles change to red instead of just snapping to the newer bigger circles.

How to align text vertically on a node within a Sankey diagram (D3.JS)?

I have modified Mike Bostok example of a Sankey diagram in D3.Js from http://bost.ocks.org/mike/sankey/ to display the value of each node as this:
Sankey http://uweb.cs.uvic.ca/~maleh2/sankey.png
node.append("text")
.attr("class","nodeValue");
///align vertically??
node.selectAll("text.nodeValue")
.attr("x", sankey.nodeWidth() / 2)
.attr("y", function (d) { return (d.dy / 2) })
.text(function (d) { return formatNumber(d.value); })
.attr("text-anchor", "middle");
//.attr("transform", "rotate(-90)")
All code from Mike's example and my modification is at jsfiddle
(http://jsfiddle.net/4xNK5/). My question is how can I display the value of the node vertically? I assumed a simple .attr("transform", "rotate(-90)"); would do it. Indeed, I get the value of the node aligned vertically BUT out of place. I cannot make sense of the logic behind it. Any ideas would be appreciated...
I am going to walk you through several experiments (all with accompanied jsfiddles) that will guide you to the solution of your problem.
Starting Point
(click for jsfiddle)
This is slightly modified example from your question:
It has been modified so that it contains data in JavaScript instead of in JSON file, and also it contains code from Sankey plugin. This is done just in order to have working example in JsFiddle. You can of course adjust it to suit your needs with respect to data etc...
The key code is here, as you already mentioned:
node.selectAll("text.nodeValue")
.attr("x", sankey.nodeWidth() / 2)
.attr("y", function (d) { return (d.dy / 2) })
.text(function (d) { return formatNumber(d.value); })
.attr("text-anchor", "middle");
Experiment 1
(click for jsfiddle)
I was really bothered that values and names of nodes are not aligned, so I added this code that slightly moves values downwards:
.attr("dy", 5)
The result is here:
Experiment 2
(click for jsfiddle)
Now lets try to add this code (I used 45 degrees on purpose to easier spot how rotation works):
.attr("transform", "rotate(-45)")
Surprisingly, we get this:
And if we examine the html/svg in firebug or similar tool, we'll notice that the reason for such rotation behavior is that value labels are actually part of other container: (and center of rotation is not center of label, but origin of that container)
Experiment 3
(click for jsfiddle)
In this experiment, I wanted to avoid using rotation at all. I wanted to use svg text property writing-mode, like this:
.style("writing-mode", "tb")
And I got this:
However, I couldn't get value labels to be oriented with its top to the left. Also, I heard this method has some Firefox compatibility problems, so I gave it up.
Solution
(click for jsfiddle)
After experiments above, my conclusion was that rotation should be done first, and that translation should be applied so that already rotated value labels are moved to the center of nodes. Since this translation is data-dependent, the code should look like this:
node.selectAll("text.nodeValue")
.text(function (d) { return formatNumber(d.value); })
.attr("text-anchor", "middle")
.attr("transform", function (d) {
return "rotate(-90) translate(" +
(-d.dy / 2) +
", " +
(sankey.nodeWidth() / 2 + 5) +
")";});
The result is:

D3 Brush (multiple brushes)

I have several bars I'd like to draw and allow the user to use a brush to select a portion of a bar. The code is simple.
I have a Fiddle setup at:
http://jsfiddle.net/N32CS/
I'm not sure if it is my scale's that are wrong or the brushes themselves...
currentG.append("g")
.attr("id", "g_" + val.curNum)
.attr("class", "x brush")
.call(brush)
.selectAll("rect")
.attr("y", yScale(arrayData[i].curNum))
.attr("height", 10);
It explains the problem I'm having of the user being able to at times drag outside the area of one bar or being limited to the area of another bar.
Thanks!
I updated your code to work as intended:
http://jsfiddle.net/N32CS/2/
var brushG = currentG.append("g")
.attr("id", "g_" + val.curNum)
.attr("class", "x brush");
var brush = d3.svg.brush();
brushG.datum({brush: brush});
...
brush.on("brushstart", function (p) {
d3.selectAll(".x.brush")
.filter(function(d) { console.log(d, d.brush != brush);return d.brush != brush; })
.each(function(d) { d3.select(this).call(d.brush.clear()) });
})
Basically I'm storing the brush function as data on each of the brush groups.
when you start brushing it clears the brushes for all the other bars and not it's own.
This is a pretty common thing, where it really helps to get used to binding data to the elements. If you bind stuff rather than keeping around global variables you can do everything with d3 selections and callbacks!

drag multiple nodes in d3 force directed layout

Precondition: d3 force directed layout;
some nodes are selected by sequential clicking one by one (visually they become bigger and in the code pushed to array)
Is there a way to drag them all by picking one with the mouse the same way as files in the Windows explorer?
P.S. I'm getting very much answers here on stackoverflow without asking for a long time. This is my first question. Thanks in advance for any help!
The way that I impemented the dragging of multiple nodes (based off of children) was by recording the displacement of the dragged node inside of my tick function with a variable whose scope allows the value to still exist the next time tick runs.
You will need an object where the key is a unique indentifier of the node being dragged and the value is a d3 selection of the nodes that you would like to translate/drag when the key node is dragged.
dragObject is the above-mentioned object.
nodeData is the d3 data of the principle node that you are dragging - ( d3.select(node uid).datum() ).
offset.x and offset.y make up the above-mentioned variable that was defined the last time tick was run.
var translateAllChildren = function (nodeData) {
if (dragObject[nodeData.uid]) {
dragObject[nodeData.uid]
.attr("transform", function(d) {
d.x = (d.x + offset.x);;
d.y = (d.y + offset.y);
return "translate(" + d.x + "," + d.y + ")";
});
}
}

Categories