I have a bunch of nodes in a circle around a centre point. I got these positions by drawing arcs first then using the arcs [X,Y] position, populated an array which was used for the positions of the nodes. Using the forcelayout from the javascript library D3.
What I want to do now, if the nodes meet a certain criteria, for example, name starts with L, move them out to the outline of a bigger circle. I have made a simple diagram to explain.
I wish to be able to move from [X2,Y2] to [X3,Y3]. I labelled [X1,Y1] as I am sure you would need this to work out the vector from x1y2 to x2,y2 wish would then be used to calculate the movement along that vector, but I'm unsure how to do this movement
I don't know if the problem is still active, but I'll answer anyway. Since the Problem has a cylindrical symmetry it is best to use polar coordinates. So x,y become r,phi whereas r = sqrt(x^2+y^2) and phi=arctan(y/x). If you want to move a point X(r,phi) in the radial direction by lets say r' you do it by simple adding it to the existing radius. Thus X'=X(r+r',phi)
Here's the way I solved it. I had a variable moveOutso I could toggle between the original node position and the one I move to. So depending on the value of moveOut I alter the scale of movement away from center.
var thisNode = circleViewNode.filter(function(d){
//console.log(d)
return d.origin != 'EquivalenceSets' && d.hasRelationship != true;
});
thisNode.each(function(d){
thisNodeSize = d.thisRadius;
});
if(!moveOut){
thisScale = innerModelRadius - thisNodeSize*1.5;
moveOut = true;
} else {
thisScale = innerModelItemRadius + (outerModelItemRadius - innerModelItemRadius)/2;
moveOut = false;
}
thisNode.each(function(d){
//console.log(d);
var centerOfCircle = [width/2,height/2]; //get center
//var centerOfCircle = [arcCenter.x, arcCenter.y];
var thisPosition = [d.x, d.y]; //get position of current node
//thisVector = [center[0]-thisPosition[0], center[1]-thisPosition[1]],
var thisVector = [thisPosition[0] - centerOfCircle[0], thisPosition[1] - centerOfCircle[1]];
var thisVectorX = thisVector[0];
var thisVectorY = thisVector[1];
var xSquared = Math.pow(thisVector[0],2);
var ySquared = Math.pow(thisVector[1],2);
var normalVector = Math.sqrt(xSquared + ySquared); //using pythagoras theorum to work out length from node pos to center
//console.log(d);
thisVectorX= thisVectorX/normalVector;
thisVectorY= thisVectorY/normalVector;
// d.x = centerOfCircle[0]+(thisVectorX*thisScale);// + -38.5;
// d.y = centerOfCircle[1]+(thisVectorY*thisScale);// + -20;
d.x = centerOfCircle[0]+(thisVectorX*thisScale); //thisScale gives the ability to move back to original pos
d.y = centerOfCircle[1]+(thisVectorY*thisScale);
//}
})
.transition().duration(1000)
.attr("transform", function(d)
{
//console.log(d.hasRelationship);
//console.log(d.y);
return "translate(" + d.x + "," + d.y + ")"; //transition nodes
});
Related
I have brought in the zoom functionality on mouse wheel to my world map in d3.
I am using the below code for that,
.on("wheel.zoom",function(){
var currScale = projection.scale();
var newScale = currScale - 2*event.deltaY;
var currTranslate = projection.translate();
var coords = projection.invert([event.offsetX, event.offsetY]);
projection.scale(newScale);
var newPos = projection(coords);
projection.translate([currTranslate[0] + (event.offsetX - newPos[0]), currTranslate[1] + (event.offsetY - newPos[1])]);
g.selectAll("path").attr("d", path);
})
.call(d3.drag().on("drag", function(){
var currTranslate = projection.translate();
projection.translate([currTranslate[0] + d3.event.dx,
currTranslate[1] + d3.event.dy]);
g.selectAll("path").attr("d", path);
}))
But my problem is that the bubbles placed on map is not getting zoomed as per the zoomin of map also the location of bubbles getting misplaced.
Below is the link for my current working code.
https://plnkr.co/edit/jHJ4R1YhI9yPLusUMLh0?p=preview
If you update your projection, you need to update all your features to reflect the new projection. You've done this with your path (this in relation to your drag event, as I was unable to scroll wheel zoom, but the principle is the same):
// update projection
var currTranslate = projection.translate();
projection.translate([currTranslate[0] + d3.event.dx, currTranslate[1] + d3.event.dy]);
// update paths
g.selectAll("path").attr("d", path);
But, you did not update the circles, as circles are not paths.
You can simply re-apply how you positioned the circles in the first place, afterall you are doing the same thing only with an updated projection:
// update projection
var currTranslate = projection.translate();
projection.translate([currTranslate[0] + d3.event.dx, currTranslate[1] + d3.event.dy]);
// update paths coordinates
g.selectAll("path").attr("d", path);
// update circle coordinates
svg.selectAll(".city-circle")
.attr("cx",function(d){
var coords = projection([d.Attribute_Longitude,d.Attribute_Latitude]);
return coords[0];
})
.attr("cy",function(d){
var coords = projection([d.Attribute_Longitude,d.Attribute_Latitude]);
return coords[1];
})
Here's an updated plunker.
One way to apply links to text is to use the force.links() array for the text elements and centre the text on the midpoint of the link.
I have some nodes with bidirectional links, which I've rendered as paths that bend at their midpoint to ensure it's clear that there's two links between the two nodes.
For these bidirectional links, I want to move the text so that it sits correctly over the bending path.
To do this, I've attempted to calculate the intersection(s) of a circle centred on the centre point of the link and a line running perpendicular to the link that also passes through its centre. I think in principal this makes sense, and it seems to be half working, but I'm not sure how to define which coordinate returned through calculating the intersections to apply to which label, and how to stop them jumping between the curved links when I move the nodes around (see jsfiddle - https://jsfiddle.net/sL3au5fz/6/).
The function for calculating coordinates of text on arcing paths is as follows:
function calcLinkTextCoords(d,coord, calling) {
var x_coord, y_coord;
//find centre point of coords
var cp = [(d.target.x + d.source.x)/2, (d.target.y + d.source.y)/2];
// find perpendicular gradient of line running through coords
var pg = -1 / ((d.target.y - d.source.y)/(d.target.x - d.source.x));
// define radius of circle (distance from centre point text will appear)
var radius = Math.sqrt(Math.pow(d.target.x - d.source.x,2) + Math.pow(d.target.y - d.source.y,2)) / 5 ;
// find x coord where circle with radius 20 centred on d midpoint meets perpendicular line to d.
if (d.target.y < d.source.y) {
x_coord = cp[0] + (radius / Math.sqrt(1 + Math.pow(pg,2)));
} else {
x_coord = cp[0] - (radius / Math.sqrt(1 + Math.pow(pg,2)));
};
// find y coord where x coord is x_text and y coord falls on perpendicular line to d running through midpoint of d
var y_coord = pg * (x_coord - cp[0]) + cp[1];
return (coord == "x" ? x_coord : y_coord);
};
Any help either to fix the above or propose another way to achieve this would be appreciated.
Incidentally I've tried using textPath to line my text up with my links but I don't find that method to be performant when displaying upward of 30-40 nodes and links.
Update: Amended above function and now works as intended. Updated fiddle here:https://jsfiddle.net/o82c2s4x/6/
You can calculate the projection of the chord to x and y axis and add it to the source node coordinates:
function calcLinkTextCoords(d,coord) {
//find chord length
var dx = (d.target.x - d.source.x);
var dy = (d.target.y - d.source.y);
var chord = Math.sqrt(dx*dx + dy*dy);
//Saggita
// since radius is equal to chord
var sag = chord - Math.sqrt(chord*chord - Math.pow(chord/2,2));
//Find the angles
var t1 = Math.atan2(sag, chord/2);
var t2 = Math.atan2(dy,dx);
var teta = t1+t2;
var h = Math.sqrt(sag*sag + Math.pow(chord/2,2));
return ({x: d.source.x + h*Math.cos(teta),y: d.source.y + h*Math.sin(teta)});
};
Here is the updated JsFiddle
Right now in my program I have both a drag function and a zoom function.
The problem can best be illustrated in this minimal test case. When the zoom is initiated at first, you may notice that the points in the graph will transition just fine. But, if you drag a point before initiating a zoom, that point will usually travel outside the scope of the graph.
This problem seems to be happening because my coordinates are defined in scaled space centered around the upper left corner. However, after an object is dragged, its coordinates are switched to pixel space, centered around its original coordinates. When a zoom occurs, it will then treat the pixel coordinates as scaled space coordinates relative to the upper left corner, which causes problems.
I would appreciate any tips or pointers and thanks in advance
Drag function:
function dragmove(d) {
var barz = document.querySelector("#visual");
var point = d3.mouse(barz),
tempP = {
x: point[0],
y: point[1]
};
if (this.nodeName === "circle") {
d3.event.sourceEvent.stopPropagation();
var useZoom = $('#zoom').is(":checked");
if (useZoom == false) {
d.usePixels = 1;
d3.select(this).attr("transform", "translate(" + (d.x = tempP.x - xRange(d.initx)) + "," + (d.y = tempP.y - yRange(d.inity)) + ")");
//events to update line to fit dots
updateXs();
redoLine();
updateBars(canvas);
}
} }
Zoom function:
function zoomOut() {
//update axis
yRange.domain([d3.min(lineData, function (d) {
return d.y - 10;
}), d3.max(lineData, function (d) {
return d.y + 10;
})])
yAxisGroup.transition().call(yAxis);
xAxisGroup.transition().attr("transform", "translate(0," + yRange(0) + ")");
//update line
d3.select(".myLine").transition()
.attr("d", lineFunc(lineData));
var c = vis.selectAll("circle")
c.transition()
.attr(circleAttrs)
}
I'm using a d3 attrTween to translate a circle over a path smoothly, similar to this example and as shown in the picture below:
The circle's transition is defined here:
function transition() {
circle.transition()
.duration(2051)
.ease("linear")
.attrTween("transform", translateAlong(path.node()))
}
And the attribute tween is shown here:
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 + ")";
};
};
}
This works well thanks to the SVG method getPointAtLength, which allows us to retrieve coordinates at different lengths of the path. However, I need a different kind of behavior and I've been unable to come up with a solution so far.
I need the circle to animate along the path, but at a steady horizontal speed. Meaning that the circle ought to take as much time to navigate this slice:
As it does with this slice:
Because both slices encompass the same width. On a low level, what I need is to be able to translate any X coordinate with its corresponding Y coordinate along the path. I've looked at all the SVG path methods and I haven't found anything particularly useful here. I'm hoping there's some way in D3 to feed an X coordinate to a d3 line and retrieve its corresponding Y coordinate.
Here's a JSFiddle working as described above. I'd really appreciate any help I can get on this. Thanks!
I ended up creating a lookup array for all my points along the line using getPointAtLength:
var lookup = [];
var granularity = 1000;
var l = path.node().getTotalLength();
for(var i = 1; i <= granularity; i++) {
var p = path.node().getPointAtLength(l * (i/granularity))
lookup.push({
x: p.x,
y: p.y
})
}
Once I had all those points in my lookup table, I used a bisector in my translate tween:
var xBisect = d3.bisector(function(d) { return d.x; }).left;
function translateAlong(path) {
var l = path.getTotalLength();
return function (d, i, a) {
return function (t) {
var index = xBisect(lookup, l * t);
var p = lookup[index];
return "translate(" + p.x + "," + p.y + ")";
};
};
}
And it works as expected! Yahoo!
Fiddle
I'm using d3 tree layout similar to this example: http://bl.ocks.org/mbostock/4339083
I implemented a search box that when typing, centers your screen on a virtual "average" position of all the appropriate nodes.
I want to adjust the scale, so that selected nodes will be
All Visible
As zoomed in as possible.
If the search match is exactly 1, simulate the clicking on the node, else center to this virtual position.
if (matches[0].length === 1) {
click(matches.datum(), 0, 0, false);
}
else {
var position = GetAveragePosition(matches);
centerToPosition(position.x, position.y, 1);
}
This is what the centerToPosition function looks like:
function centerToPosition(x0, y0, newScale) {
if (typeof newScale == "undefined") {
scale = zoomListener.scale();
}
else {
scale = newScale;
}
var x = y0 * -1; //not sure why this is.. but it is
var y = x0 * -1;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
So how can I calculate the new scale? I tried different variations by taking the extents of the data points
var xExtent = d3.extent(matches.data(), function (d) {
return d.x0;
});
var yExtent = d3.extent(matches.data(), function (d) {
return d.y0;
});
Also tried looking at the transform properties of the group before centering the screen.
var components = d3.transform(svgGroup.attr("transform"));
I'll try to add a js fiddle soon!
EDIT: Here it is: http://jsfiddle.net/7SJqC/
Interesting project.
The method of determining the appropriate scale to fit a collection of points is fairly straightforward, although it took me quite a while to figure out why it wasn't working for me -- I hadn't clued in to the fact that (since you were drawing the tree horizontally) "x" from the tree layout represented vertical position, and "y" represented horizontal position, so I was getting apparently arbitrary results.
With that cleared up, to figure out the zoom you simply need to find the height and width (in data-coordinates) of the area you want to display, and compare that with the height and width of the viewport (or whatever your original max and min dimensions are).
ScaleFactor = oldDomain / newDomain
Generally, you don't want to distort the image with different horizontal and vertical scales, so you figure out the scale factor separately for width and height and take the minimum (so the entire area will fit in the viewport).
You can use the d3 array functions to figure out the extent of positions in each direction, and then find the middle of the extent adding max and min and dividing by two.
var matches = d3.selectAll(".selected");
/*...*/
if ( matches.empty() ) {
centerToPosition(0, 0, 1); //reset
}
else if (matches.size() === 1) {
click(matches.datum(), 0, 0, false);
}
else {
var xExtent = d3.extent(matches.data(), function (d) {
return d.x0;
});
var yExtent = d3.extent(matches.data(), function (d) {
return d.y0;
});
//note: the "x" values are used to set VERTICAL position,
//while the "y" values are setting the HORIZONTAL position
var potentialXZoom = viewerHeight/(xExtent[1] - xExtent[0] + 20);
var potentialYZoom = viewerWidth/(yExtent[1] - yExtent[0] + 150);
//The "20" and "150" are for height and width of the labels
//You could (should) replace with calculated values
//or values stored in variables
centerToPosition( (xExtent[0] + xExtent[1])/2,
(yExtent[0] + yExtent[1])/2,
Math.min(potentialXZoom, potentialYZoom)
);
}
http://jsfiddle.net/7SJqC/2/