Get coordinates of clicked on node in d3 tree and centre - layout

I have a tree with on click event listeners. I'd like to re-centre my tree on whichever node the user clicked.
How do I get the actual x / y values of a tree node inside the click event?

To get the x/y you must translate the nodes back through whatever translation you've already applied:
http://jsfiddle.net/WLaVU shows a working example of what you want.
// click event handler
function click_handler(d)
{
// these dudes must be smooshed back through the same transform
var x = xs(d);
var y = ys(d);
// normalize for width/height
var new_x = (-x + (width / 2));
var new_y = (-y + (height / 2));
// move the main container g
svg.attr("transform", "translate(" + new_x + "," + new_y + ")");
}

Related

Change starting point of alongPath animation

I have a simple path element that a circle has to follow using D3.js. I found a way to do this through using getTotalLength and getPointAtLength methods on SVG as described in this example:
http://bl.ocks.org/mbostock/1705868
It works fine but the problem is that my circle starts at the bottom and follows the line upwards, how do i change the starting point of the circle in the animation so it goes from top to bottom? Here's my code:
my path element:
<path id="testpath" style="fill:none;stroke:#FFFFFF;stroke-width:0.75;" d="M1312.193,1035.872l80.324-174.458l13.909-264.839l507.09-211.095
l8.667-248.405" />
the D3 code:
function followPath()
{
var circle = d3.select("svg").append("circle")
.attr("r", 6.5)
.attr("transform", "translate(0,0)")
.attr("class","circle");
var path = d3.select("#testpath");
function transition()
{
circle.transition()
.duration(10000)
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
}
function translateAlong(path)
{
var l = path.getTotalLength();
return function (d, i, a) {
return function (t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
transition();
}
I made a fiddle where u can see it going from bottom to top(i know the viewbox is big, it's part of a much bigger SVG image)
http://jsfiddle.net/6286X/3/
The animation will start at the start of the line, as defined in the SVG. To make it start at an arbitrary point, you have to offset it accordingly. To get the exact opposite as in your case, the change is almost trivial though -- you just start at the end and move towards the beginning. That is, you "invert" the transition by considering 1-t instead of t:
return function (t) {
var p = path.getPointAtLength((1-t) * l);
return "translate(" + p.x + "," + p.y + ")";
};
Complete demo here.

D3 force layout with CSS positioning

I'm placing circles on a map corresponding to GPS coordinates. Each circle is contained within an svg container which is placed on the page using CSS top and left properties. In my implementation, these containers often sit atop one another.
I am trying to implement collision detection and/or add a slight negative charge to these containers so that overlaps cause containers to distance themselves from one another.
Thus far, my tests with force layouts have either resulted in no change, or resulted in an error ('cannot set property index of null' or 'cannot set property x of null'). It's apparent that I'm doing something wrong but I have been unable to identify a path to resolution from the articles I've read online.
Any ideas on how I can stop the containers from sitting atop one another?
var self = this;
var data = [{lat: 127, lon: 36, name: 'a', radius: 9},{lat:127, lon: 36, name: 'b', radius: 9}];
// Position SVG containers correctly
var latLngToPx = function(d) {
var temp = new google.maps.LatLng(d.lat, d.lon);
temp = self.map.projection.fromLatLngToDivPixel(temp);
d.x = temp.x;
d.y = temp.y;
return d3.select(this)
.style('left', d.x + 'px')
.style('top', d.y + 'px');
};
var collide = function(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * 0.5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
var svgBind = d3.select(settings[type].layer).selectAll('svg')
.data(data, function(d){ return d.name; })
.each(latLngToPx);
var svg = svgBind.enter().append('svg')
.each(latLngToPx)
// svg[0] contains the svg elements
var nodes = svg[0];
var force = d3.layout.force()
.nodes(nodes)
.charge(-100)
.start();
force.on('tick', function(){
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) {
q.visit(collide(nodes[i]));
}
svg
.style('left', function(d){ return (d.x - lm.config.offset) + 'px';})
.style('top', function(d){ return (d.y - lm.config.offset) + 'px';});
});
var circ = svg.append('circle')
.attr('r', settings[type].r)
.attr('cx',10)
.attr('cy',10)
You shouldn't need to do the collision detection yourself -- the force layout should take care of that for you. Here are the basic steps you need to take.
To each data element that represents a circle, add x and y members that contain their current (screen) coordinates. This is what the force layout will operate on.
Pass the array of these elements to the force layout as nodes. There's no need to set links to start with, although you might want to do so later to control the placement of nodes with respect to each other.
Start the force layout.
For each tick, redraw the elements at the appropriate position.
Tweak the parameters of the force layout to your liking.
You are doing most of this already, I'm just mentioning it again to clarify. The code would look something like this.
function latLngToPx(d) {
var temp = new google.maps.LatLng(d.lat, d.lon);
temp = self.map.projection.fromLatLngToDivPixel(temp);
d.x = temp.x;
d.y = temp.y;
};
data.forEach(function(d) { latLngToPx(d); });
var nodes = d3.select("body").selectAll("svg").data(data).enter().append("svg");
var force = d3.layout.force().nodes(data);
force.on("tick", function() {
nodes.style('left', function(d){ return (d.x - lm.config.offset) + 'px';})
.style('top', function(d){ return (d.y - lm.config.offset) + 'px';});
});

How do I translate a set of svg elements under a g when dragging?

JSFiddle: http://jsfiddle.net/Umugb/19/
I've written the code below to drag a set of elements around on an svg. Each g element has a circle that is draggable (but otherwise nothing else is). However, when I drag the circle, I'd like for the rest of the g to translate with it.
The problem is that the dragging goes haywire. If I take out the transform and instead just change the circle's cx and cy to the new_cx and new_cy, then the circle is movable as expected. However, using transform, all of the elements fly off the page with d3.event.dx/dy of abnormally large absolute values.
Is there anything obviously wrong with the code below? Or does anyone have further insight into why this is happening?
var drag = d3.behavior.drag().on("drag", translate);
function translate() {
var g = $("#" + this.parentNode.id);
var circle = d3.select(this);
var r = parseInt(circle.attr("r"));
var box_width = Math.max(g[0].getElementsByClassName('circletext')[0].getBBox().width, 2*r);
var cx = parseInt(circle.attr("cx"));
var cy = parseInt(circle.attr("cy"));
if (g.attr("transform")) {
var translate = g.attr("transform").split('translate')[1].split(',');
cx = cx + parseInt(translate[0].slice(1));
cy = cy + parseInt(translate[1].slice(0,-1));
}
var new_cx = Math.max(r, Math.min(width - box_width + r, d3.event.dx + cx));
var new_cy = Math.max(r, Math.min(height - r, d3.event.dy + cy));
d3.select("#" + this.parentNode.id).
attr("transform",
"translate(" + (new_cx-cx) + "," + (new_cy-cy) + ")");
EDIT: Updated with JSFiddle at top

Get arrowheads to point at outer edge of node in D3

I'm new to D3 and I'm trying to create an interactive network visualization. I've copied large parts of this example, but I have changed the curved lines to straight ones by using SVG "lines" rather than "paths", and I've also scaled the nodes according to the data they represent. The problem is that my arrowheads (created with SVG markers) are at the ends of the lines. Since some of the nodes are large, the arrows get hidden behind them. I'd like my arrowheads to show up right at the outside edge of the node they point to.
Here is how I'm creating the markers and links:
svg.append("svg:defs").selectAll("marker")
.data(["prereq", "coreq"])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link")
.attr("marker-end", function(d) { return "url(#" + d.type + ")"; });
I noticed that the "refX" attribute specifies how far from the end of the line the arrowhead should show up. How can I make this dependent on the radius of the node it's pointing to? If I can't do that, could I instead change the endpoints of the lines themselves? I'm guessing I would do that in this function, which resets the endpoints of the lines as everything moves:
function tick() {
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; });
circle.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
text.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
Which approach makes more sense, and how would I implement it?
Thanks Lars Kotthoff, I got this to work following the advice from the other question! First I switched from using lines to paths. I don't think I actually had to do that, but it made it easier to follow the other examples I was looking at because they used paths.
Then, I added a "radius" field to my nodes. I just did this when I set the radius attribute, by adding it as an actual field rather than returning the value immediately:
var circle = svg.append("svg:g").selectAll("circle")
.data(force.nodes())
.enter().append("svg:circle")
.attr("r", function(d) {
if (d.logic != null) {
d.radius = 5;
} else {
d.radius = node_scale(d.classSize);
}
return d.radius;
I then edited my tick() function to take this radius into account. This required a bit of simple geometry...
function tick(e) {
path.attr("d", function(d) {
// Total difference in x and y from source to target
diffX = d.target.x - d.source.x;
diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
// x and y distances from center to outside edge of target node
offsetX = (diffX * d.target.radius) / pathLength;
offsetY = (diffY * d.target.radius) / pathLength;
return "M" + d.source.x + "," + d.source.y + "L" + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
});
Basically, the triangle formed by the path, it's total x change (diffX), and it's total y change (diffY) is a similar triangle to that formed by the segment of the path inside the target node (i.e. the node radius), the x change inside the target node (offsetX), and the y change inside the target node (offsetY). This means that the ratio of the target node radius to the total path length is equal to the ratio of offsetX to diffX and to the ratio of offsetY to diffY.
I also changed the refX value to 10 for the arrows. I'm not sure why that was necessary but now it seems to work!
I answered the same question over here. The answer uses vector math, it's quite useful for other calculations as well.

How do I adjust my SVG transform based on the viewport?

I'm working with the d3 library and have had success working with the chloropleth example, as well as getting a click action to zoom in to a particular state (see this question for details). In particular, here is the code I'm using for my click to zoom event on a state:
// Since height is smaller than width,
var baseWidth = 564;
var baseHeight = 400;
d3.selectAll('#states path')
.on('click', function(d) {
// getBBox() is a native SVG element method
var bbox = this.getBBox(),
centroid = [bbox.x + bbox.width/2, bbox.y + bbox.height/2],
// since height is smaller than width, I scale based off of it.
zoomScaleFactor = baseHeight / bbox.height,
zoomX = -centroid[0],
zoomY = -centroid[1];
// set a transform on the parent group element
d3.select('#states')
.attr("transform", "scale(" + scaleFactor + ")" +
"translate(" + zoomX + "," + zoomY + ")");
});
However, when I click to view on the state, my transform is not in the center of my viewport, but off to the top left, and it might not have the proper scale to it as well. If I make minor adjustments manually to the scaleFactor or zoomX/zoomY parameters, I lose the item altogether. I'm familiar with the concept that doing a scale and transform together can have significantly different results, so I'm not sure how to adjust.
The only other thing I can think of is that the original chloropleth image is set for a 960 x 500 image. To accomodate for this. I create an albersUSA projection and use my d3.geo.path with this projection and continue to add my paths accordingly.
Is my transform being affected by the projection? How would I accomodate for it if it was?
The scale transform needs to be handled like a rotate transform (without the optional cx,cy parameters), that is, the object you want to transform must first be moved to the origin.
d3.select('#states')
.attr("transform",
"translate(" + (-zoomX) + "," + (-zoomY) + ")" +
"scale(" + scaleFactor + ")" +
"translate(" + zoomX + "," + zoomY + ")");
For futher reference,
I found this article where you should find how to use the matrix transformation to achieve zoom and pan effects very simple.
Excerption:
<script type="text/ecmascript">
<![CDATA[
var transMatrix = [1,0,0,1,0,0];
function init(evt)
{
if ( window.svgDocument == null )
{
svgDoc = evt.target.ownerDocument;
}
mapMatrix = svgDoc.getElementById("map-matrix");
width = evt.target.getAttributeNS(null, "width");
height = evt.target.getAttributeNS(null, "height");
}
]]>
</script>
function pan(dx, dy)
{
transMatrix[4] += dx;
transMatrix[5] += dy;
newMatrix = "matrix(" + transMatrix.join(' ') + ")";
mapMatrix.setAttributeNS(null, "transform", newMatrix);
}
function zoom(scale)
{
for (var i=0; i<transMatrix.length; i++)
{
transMatrix[i] *= scale;
}
transMatrix[4] += (1-scale)*width/2;
transMatrix[5] += (1-scale)*height/2;
newMatrix = "matrix(" + transMatrix.join(' ') + ")";
mapMatrix.setAttributeNS(null, "transform", newMatrix);
}

Resources