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

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);
}

Related

moving a circle along a d3 path animating at varying speeds

I really found this question and answer helpful on how to get a line animate at varying speeds.
Changing speed of D3 path animation
Which pointed to this block:
http://bl.ocks.org/explunit/6082362
I've been following this and would like to add an circle which moves along the start of the line.
I've added a marker
var marker = g.append("circle")
.attr("r", 7)
.attr("id", "marker")
but for the life of me I can't get it to follow along the line, at the same speed.
I've added this bit to the end of that block
var p = path.node().getPointAtLength(lengthAt[i-1] );
markerTransition = markerTransition.transition()
.duration(lineData[i].speed)
.ease('linear')
.attr("transform", "translate(" + p.x + "," + p.y + ")");
and now I have a moving circle, but it's not in sync with the line and is located at different coordinates for some reason.
How can I get my circle to correctly follow along the line at (varying speeds)?
UPDATE:
Almost there!
I've added a jsfiddle: http://jsfiddle.net/mbrownshoes/k86fbade/6/
Circle is moving at the correct speed to first point, now I need the circle to start each transition from the previous point and not from the beginning.
Solved (though going about it another way)
http://jsfiddle.net/mbrownshoes/ozpt6dn7/2/
for (var i = 0; i < lineData.length - 1; ++i) {
wait[i] = tottime;
tottime += lineData[i].t;
setTimeout(function () {
temp[0] = lineData[ipath];
temp[1] = lineData[ipath + 1];
time = lineData[ipath].t;
var lineGraph = ss.append("path")
.attr("d", lineFunction(temp))
.attr("stroke", "grey")
.attr("stroke-width", 3)
.attr("fill", "none");
var totalLength = lineGraph.node().getTotalLength();
console.log(totalLength);
console.log(ipath + " " + temp[0].x + " " + temp[1].x + " " + time);
lineGraph.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(time)
.ease("linear")
.attr("stroke-dashoffset", 0);
circle.transition()
.duration(time)
.ease("linear")
.attr("transform",
function () {
return "translate(" + temp[1].x + "," + temp[1].y + ")";
});
console.log(ipath + ": " + time + ", " + wait);
ipath++;
}, wait[i]);
}
Thanks to https://groups.google.com/forum/m/#!topic/d3-js/UhaN7HdYTWM

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.

Get coordinates of clicked on node in d3 tree and centre

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 + ")");
}

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.

Drawing a portion of an ellipse programmatically with a Bezier or an Elliptical path — SVG and raphael.js

I'm trying to draw a bezier curve surrounding an ellipse with a given margin :
I want to achieve this programmatically, so if I changes the ellipse size, the curve will follow it.
At the moment I've made this function :
function bezierPathTopRounded(ellipse, margin) {
var box = ellipse.paper.getBBox();
var leftX = box.x - margin;
var rightX = box.x + margin + box.width;
var y = box.y + box.height/2 - margin;
var p = "M "+ leftX + ", "+ y
+ " C " //could be relative too
+ ( box.x - margin + (box.width/15) ) + ", " + ( box.y + margin - (box.height/4.5) ) + " "
+ ( box.x + margin + box.width - (box.width/15) ) + ", " + ( box.y + margin - (box.height/4.5) ) + " "
+ rightX +", " + y;
return p;
}
But I can't figure out how to calculate this direction points values so that it will work with any ellipse :
box.width/15
box.height/4.5
There is a fiddle with this example.
I've read this stackoverflow question and I tried the same on my example, but still can't figure out a simple solution, it remains random...
Edit
Now I'm trying with an elliptical Arc, the result is worser than with a Bezier Path :
There is the function I'm using. If I remove the margin it follows exactly my ellipse... Finally this is the matter is how may I follow the ellipse with a margin ?
function borderPath(ellipse, margin, flag) {
var flag = flag == undefined ? 1 : 0;
var box = ellipse.paper.getBBox();
var leftX = box.x - margin;
var rightX = box.x + margin + box.width;
var y = box.y + box.height/2;
y += (flag == 1) ? -margin : margin;
var rx = box.width/2 + margin;
var ry = box.height/2;
var p = "M "+ leftX + ", "+ y
+ " A "
+ rx + " " + ry
+ " 0 0 "+ flag +" "
+ rightX +", " + y;
return p;
}
See the updated fiddle here.
Really sorry for the awful colors, those are for example purpose.
If you want to do this with Bezier curves, you'll have to decide how "wrong" you want it to look. Bezier curves cannot represent circular (and by extension, elliptical) curves, they can only get really close, in the same way a polygon can, just with higher precision using fewer sections.
I describe both circle-approximation and curve offsetting using Bezier curves in my primer on Bezier curves, http://pomax.github.io/bezierinfo/#circles_cubic and http://pomax.github.io/bezierinfo/#offsetting respectively, but if you're coding this from scratch particularly the offsetting will be overkill if you only need it for what you describe in your example.
Instead, I'd recommend firing up Inkscape or Illustrator, turning on the grid overlay, and drawing a bezier curve around your ellipse. Make it look good, then check what the coordinates are, and use that as reliable-enough information for implementing in your canvas program. You probably don't need mathematically rigidly correct, as long as people don't go "that looks wrong", you should be just fine.
I've manage to make an elliptical arc according to the ellipse and its margin.
Than i'm simply hiding the part I don't want with a rectangle.
Here is the function :
function borderPath(ellipse, flag) {
var flag = flag == undefined ? 1 : flag;
var box = ellipse.paper.getBBox();
var leftX = box.x;
var rightX = box.x + box.width;
var y = box.y + box.height/2;
var rx = box.width/2;
var ry = box.height/2;
var p = "M "+ leftX + ", "+ y
+ " A "
+ rx + " " + ry
+ " 0 0 "+ flag +" "
+ rightX +", " + y;
return p;
}
Using bezier curves to draw elliptical path may cause you headaches. As you said in a comment, you are using path arc which works well with RaphaelJS.
Documentation about all the values it expects, especially the flags, can be found at http://www.svgbasics.com/arcs.html .

Resources