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
Related
I would like to add different shapes depending on one of the properties in my json file. I found this approach by Mike:
https://groups.google.com/forum/#!topic/d3-js/4EJDu1xOh8Y
The idea is great, I'm just not sure how to adapt it. I either want to add a circle or an svg:use element (with attr("xlink:href")). They both have (of course) different attributes. So how do I do that? What do I append? In the example, the attr("d") was also used, do I need that also?
That's what I have so far but I'm not sure what to add where.
I really appreciate your help!
var type = d3.scale.ordinal()
.domain(["Q", "C"])
.range("circle","svg:use");
for(l=0;l<4;l++){
currentsvg.selectAll("path")
.data(queryArray[l])
.enter()
.append("svg:path")
.type(function(d,i) {
if (queryArray[l][i].name.substr(0,1) == "Q"){
return type("Q");
}
else if (queryArray[l][i].name.substr(0,1) == "C"){
return type("C");
}
});
}
Below is a different solution without filtering that uses the path to draw the shapes. It doesn't use the "rect" or "circle" of svg but rather just uses the path to draw the shapes. Check out here for more on paths. Note that the circle is two connecting arcs. It also classes each shape based on the data so you can have different colors, etc using CSS. Here is a fiddle.
currentsvg.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d",function(d,i){
var path,
s = i*50,
r = 10,
w = r*2;
if (data[i] == "Q"){
path = "M" + s + " " + s + " L" + s + " " + (s+w) +
" L" + (s+w) + " " + (s+w) + " L" + (s+w) + " " + s + "Z"
}
else if (data[i] == "C"){
path = "M" + s + " " + s + " m" + -r + ", 0 " +
" a " + r + "," + r + " 0 1,0 " + r*2 + ",0" +
" a " + r + "," + r + " 0 1,0 "+ -r*2 + ",0"
}
return path;
})
.attr("class", function(d){return d == "Q" ? "rec" : "circ";})
The best way to do that is to filter the data how you want into separate data sets for each shape before you create shapes. Then you can create the shapes with that new data set.
var data = ["Q","Q","Q","C","C","Q","Q","C","Q","C"];
var circleSet = data.filter(function(d){return d === "Q";}),
squareSet = data.filter(function(d){return d === "C";});
As Lars said, that is also not how the d attribute works. Here is a working JSFiddle of the whole thing.
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 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 .
I'm trying to build a d3 bar chart, but due to a sneaky designer, I need rounded corners on the top-left and -right of each bar. I think I'm getting somewhere, but I could use a bit of help to push it over the line.
Still very much on the uphill curve learning about d3 and svg, so I hope I haven't missed anything obvious.
I have successfully made this work using the rects (commented out section), but there's a flaw, probably in how I'm drawing the paths. The anonymous functions I'm passing as arguments into the topRoundedRect function (e.g. function(datum, index) { return x(index); }) are not being evaluated before being appended to the d attribute of the path, and I don't understand why. So I end up getting an error like this:
Error: Problem parsing d="Mfunction (datum, index) { return x(index); }3,function (datum) { return height - y(datum); }h34a3,3 0 0 1 3,3vNaNh-40vNaNa3,3 0 0 1 3,-3z"
Any advice you can offer would be fantastic. Code below.
var data = [60.45,60.45,89.54,120.34,106.45,127.43];
var barWidth = 40;
var width = (barWidth + 10) * data.length;
var height = 500;
var x = d3.scale.linear().domain([0, data.length]).range([0, width]);
var y = d3.scale.linear().domain([0, d3.max(data, function(datum) { return datum; })]).
rangeRound([0, height]);
// add the canvas to the DOM
var barDemo = d3.select("body").
append("svg").
attr("width", width).
attr("height", height);
function topRoundedRect(x, y, width, height, radius) {
return "M" + (x + radius) + "," + y
+ "h" + (width - (radius * 2))
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius
+ "v" + (height - radius)
+ "h" + (0-width)
+ "v" + (0-(height-radius))
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + -radius
+ "z";
}
/*
barDemo.selectAll("rect").
data(data).
enter().
append("svg:rect").
attr("x", function(datum, index) { return x(index); }).
attr("y", function(datum) { return height - y(datum); }).
attr("height", function(datum) { return y(datum); }).
attr("width", barWidth).
attr("fill", "url(#barGradient)"); */
barDemo.selectAll("path").
data(data).
enter().append("path").
attr("d", topRoundedRect(
function(datum, index) { return x(index); },
function(datum) { return height - y(datum); },
barWidth,
function(datum) { return y(datum); },
3))
.style("fill","#ffcc00")
.style("stroke","none");
The argument for your attribute should be a function that takes the datum and index as parameters. Try:
attr("d", function(datum, index) {
return topRoundedRect( x(index),
height - y(datum),
barWidth,
y(datum),
3);
})
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);
}