I am trying to implement a version of the SVG Semantic Zoom and Pan example for D3.js, found here. I am trying to do this with a Dendrogram / tree (example here), as recommended by Mike Bostock (here). The goal is like this jsFiddle that one of the other threads mentioned, except without the weird node / path unlinking behavior. My personal attempt is located here.
I was getting an error in Firebug with Mike's code, about "cannot translate(NaN,NaN)", so I changed the code in the zoom function to what is shown below. However, the behavior is weird. Now 1) my paths don't zoom / move, and 2) I can only pan the nodes from Lower Right--Upper Left (if I try panning from Lower Left--Upper Right, the nodes still move along the LR-UL direction).
var vis = d3.select("#tree").append("svg:svg")
.attr("viewBox", "0 0 600 600")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
.call(d3.behavior.zoom().x(x).y(y).scaleExtent([1,8]).on("zoom",zoom));
// zoom in / out
function zoom() {
var nodes = vis.selectAll("g.node");
nodes.attr("transform", transform);
}
function transform(d) {
return "translate(" + x(d.y) + "," + y(d.x) + ")";
}
I tried following the other solutions given in the Google Groups thread mentioned above and the jsFiddle, but I don't make much progress. Including the path links from the jsFiddle in my code and a translate() function lets me scale the paths--except 1) they flip vertically (somewhere x and y transposition isn't working right); 2) the paths don't zoom at the same rate as the nodes (perhaps related to #1), so they become "unlinked"; and 3) when I pan, the paths now pan in all directions, but the nodes don't move. When I click on a node to expand the tree, the paths re-adjust and fix themselves, so the update code seems to work better (but I don't know how to identify / copy the working parts of that). See my code here.
function zoom(d) {
var nodes = vis.selectAll("g.node");
nodes.attr("transform", transform);
// Update the links...
var link = vis.selectAll("path.link");
link.attr("d", translate);
}
function transform(d) {
return "translate(" + x(d.y) + "," + x(d.x) + ")";
}
function translate(d) {
var sourceX = x(d.target.parent.y);
var sourceY = y(d.target.parent.x);
var targetX = x(d.target.y);
var targetY = (sourceX + targetX)/2;
var linkTargetY = y(d.target.x0);
var result = "M"+sourceX+","+sourceY+" C"+targetX+","+sourceY+" "+targetY+","+y(d.target.x0)+" "+targetX+","+linkTargetY+"";
//console.log(result);
return result;
My questions are:
Are there any working examples of zoom / pan on a Dendrogram / tree that I can look at?
With the code I have above, can anyone identify where / how the paths are getting flipped? I have tried various permutations within the translate() function of drawing the SVG Cubic Bezier curve, but nothing seems to work right. Again, my jsFiddle is here.
Any troubleshooting tips or ideas why panning only partially works?
Thank you everyone for your help!
You had a pretty good implementation that was derailed by one major typo:
function transform(d) {
return "translate(" + x(d.y) + "," + x(d.x) + ")";
}
Should have been
function transform(d) {
return "translate(" + x(d.y) + "," + y(d.x) + ")";
}
To have your paths not flip you should have probably not reversed the y-axis:
y = d3.scale.linear().domain([0, h]).range([h, 0])
changed to
y = d3.scale.linear().domain([0, h]).range([0, h])
Fixes are here: http://jsfiddle.net/6kEpp/2/. But for future reference, you should probably have your x-axis operate on x-values, and y-axis operate on y-values, or you're just going to really confuse yourself.
Final remarks to polish your implementation:
There is a little bit of jumpiness in the bezier drawing going from the default rendering (or right after opening/closing a node) to the zoom implementation. You just need to make those the same bezier function, and it will feel a lot better when you play with it.
You need to update the zoom vector after the graph redraws itself, based on the relative movement of existing nodes. Otherwise, there will be a sudden jump when you try to zoom again after opening/closing a node.
Related
For a project I'm trying to draw a moving line in Phaser. I initially drew it using game.debug.geom(line), but that is not really the right way to do it, since it doesn't allow for styling, and because the debugger takes a toll in performance.
Reading the docs, it seems to me that the way to do it would be with a Phaser.Graphics object, but I haven't been able to get it to work. As an example, I tried making a line move as the hand of a clock, with one end fixed and the other moving around it.
I thought it would be fine to create the Graphics object in the middle and then in update use reset to clear it and bring it back to the center, and then lineTo to make the rest of the line. But instead what I get is a line coming outwards from the centre, and then a ring.
Picture for sadness:
I made a pen with my attempts. The code is repeated below. What I would like to have is a line (lines?) coming from the center of the circle to the points in the circumference.
Is a Graphics object the best way to do that? How do I do it?
Demo.prototype = {
create: function() {
this.graphics = game.add.graphics(
game.world.centerX,
game.world.centerY
);
this.graphics.lineStyle(2, 0xffd900);
this.counter = 0;
this.step = Math.PI * 2 / 360;
this.radius = 80;
},
update: function() {
this.graphics.reset(
this.game.world.centerX,
this.game.world.centerY
);
var y = this.radius * Math.sin(this.counter);
var x = this.radius * Math.cos(this.counter);
this.graphics.lineTo(x, y);
this.counter += this.step;
}
};
You may want to check out this Phaser game called Cut It (not my game btw, found it here).
It also draws a variable length dotted line by cleverly using the Phaser.TileSprite, and then changing its width.
TileSprite draws a repeating pattern, and you can use this to draw a line by drawing one bitmap of a linepart segment, use that as background of the TileSprite and make the height of the TileSprite the same as the height of the bitmap.
You can take a look at the game's source code, it's compressed and minified but still somewhat readable. You can look for the variable called cut_line.
I finally understood that the coordinates taken by the Phaser.Graphics object are local, respective to the object's internal coordinate system. Using moveTo(0, 0) has the desired result of moving the object's drawing pointer back to its origin (and not, as I initially thought, to the origin of the game world). Using reset(0, 0), on the other hand, would have the effect of moving the object's origin to the world's origin.
As for deleting the previous lines, the only method I've found is to manually clear the object's graphicsData Array (short of calling destroy() and creating an entirely new object, which is probably not a very good idea).
Replacing the code in the original question with this does the trick:
Demo.prototype = {
create: function() {
this.graphics = game.add.graphics(
game.world.centerX,
game.world.centerY
);
this.graphics.lineStyle(2, 0xffd900);
this.counter = 0;
this.step = Math.PI * 2 / 360;
this.radius = 80;
},
update: function(){
// Erases the previous lines
this.graphics.graphicsData = [];
// Move back to the object's origin
// Coordinates are local!
this.graphics.moveTo( 0, 0 );
var y = this.radius * Math.sin(this.counter);
var x = this.radius * Math.cos(this.counter);
this.graphics.lineTo(x, y);
this.counter += this.step;
}
};
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.
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.
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);
}
I'm responsible for delivering pages to display primary results for the US elections State by State. Each page needs a banner with an image of the State, approx 250px by 250px. Now all I need to do is figure out how to serve / generate those images...
I've dug into the docs / examples for Protovis and think I
could probably lift the State coordinate outlines- I would have to
manually transform the coordinate data to be justified and sized
properly (ick)
At the other end of the clever/brute spectrum is an enormous sprite
or series of sprites. Even with png 8 compression the file size of
a grid of 50 non-overlapping 250x250px sprites is a concern, and
sadly such a file doesn't seem to exist so I'd have to create it
from hand. Also unpleasant.
Who's got a better idea?
Answered: the right solution is to switch to d3.
What we hacked in for now:
drawStateInBox = function(box, state, color) {
var w = $("#" + box).width(),
h = $("#" + box).height(),
off_x = 0,
off_y = 0;
borders = us_lowres[state].borders;
//Preserve aspect ratio
delta_lat = pv.max(borders[0], function(b) b.lat) - pv.min(borders[0], function(b) b.lat);
delta_lng = pv.max(borders[0], function(b) b.lng) - pv.min(borders[0], function(b) b.lng);
if (delta_lat / h > delta_lng / w) {
scaled_h = h;
scaled_w = w * delta_lat / delta_lng;
off_x = (w - scaled_w) / 2;
} else {
scaled_h = h * delta_lat / delta_lng;
scaled_w = w;
off_y = (h - scaled_h) / 2;
}
var scale = pv.Geo.scale()
.domain(us_lowres[state].borders[0])
.range({x: off_x, y: off_y},
{x: scaled_w + off_x, y: scaled_h + off_y});
var vis = new pv.Panel(state)
.canvas(box)
.width(w)
.height(h)
.data(borders)
.add(pv.Line)
.data(function(l) l)
.left(scale.x)
.top(scale.y)
.fillStyle(function(d, l, c) {
return(color);
})
.lineWidth(0)
.strokeStyle(color)
.antialias(false);
vis.render();
};
d3 seems to have the capability to do maps similar to what you want. The example shows both counties and states so you would just omit the counties and then provide the election results in the right format.
There is a set of maps on 50states.com, e.g. http://www.50states.com/maps/alabama.htm, which is about 5KB. Roughly, then, that's 250KB for the whole set. Since you mention using these separately, there's your answer.
Or are you doing more with this than just showing the outline?