Using the d3 graphics library, I can't seem to make paths draw slowly so they can be seen growing.
This site has a perfect example in the "Line Chart (Unrolling)" section, but no code is given for that section. Could someone please help me with the lines of D3 code that could make that happen?
When I try appending delay() or duration() such as in the following code snippet, the path still draws immediately, And all the SVG code after this segment fails to render.
var mpath = svg.append ('path');
mpath.attr ('d', 'M35 48 L22 48 L22 35 L22 22 L35 22 L35 35 L48 35 L48 48')
.attr ('fill', 'none')
.attr ('stroke', 'blue')
.duration (1000);
A common pattern when animating lines in svg is setting a stroke-dasharray of the length of the path and then animate stroke-dashoffset:
var totalLength = path.node().getTotalLength();
path
.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(2000)
.ease("linear")
.attr("stroke-dashoffset", 0);
You can see a demo here:
http://bl.ocks.org/4063326
I believe the "D3 way" to do this is with a custom tween function. You can see a working implementation here: http://jsfiddle.net/nrabinowitz/XytnD/
This assumes that you have a generator called line set up with d3.svg.line to calculate the path:
// add element and transition in
var path = svg.append('path')
.attr('class', 'line')
.attr('d', line(data[0]))
.transition()
.duration(1000)
.attrTween('d', pathTween);
function pathTween() {
var interpolate = d3.scale.quantile()
.domain([0,1])
.range(d3.range(1, data.length + 1));
return function(t) {
return line(data.slice(0, interpolate(t)));
};
}
The pathTween function here returns an interpolator that takes a given slice of the line, defined by how far we are through the transition, and updates the path accordingly.
It's worth noting, though, that I suspect you'd get better performance and a smoother animation by taking the easy route: put a white rectangle (if your background is simple) or a clipPath (if your background is complex) over the line, and transition it over to the right to reveal the line underneath.
Based on the post that you link to, I came up with the following example:
var i = 0,
svg = d3.select("#main");
String.prototype.repeat = function(times) {
return (new Array(times + 1)).join(this);
}
segments = [{x:35, y: 48}, {x: 22, y: 48}, {x: 22, y: 35}, {x: 34, y:35}, {x: 34, y:60}];
line = "M"+segments[0].x + " " + segments[0].y
new_line = line + (" L" + segments[0].x + " " + segments[0].y).repeat(segments.length);
var mpath = svg.append ('path').attr ('d',new_line )
.attr ('fill', 'none')
.attr ('stroke', 'blue')
for (i=0; i<segments.length; i++)
{
new_segment = " " + "L"+segments[i].x + " " + segments[i].y
new_line = line + new_segment.repeat(segments.length-i)
mpath.transition().attr('d',new_line).duration(1000).delay(i*1000);
line = line + new_segment
}
It is a bit ugly, but works. You can see it on jsFiddle
Related
Related to this example (d3.j radial tree node links different sizes), I was wondering if it is possible to mix radial trees and straight-line trees in d3.js.
For my jsFiddle example: https://jsfiddle.net/j0kaso/fow6xbdL/ I would like to have the parent (level0) having a straight line to the first child (level1) and afterward the radial curved tree (as it is right now).
Is this possible?
I couldn't find anything related to it but as I'm relatively new to d3.js/JS I maybe just missed the right keywords. Hope somebody has a working example or could point me in the right direction - anyway I appreciate any hints & comments!
Where the link's source's depth is 0, then you can generate a SVG path from the link's source and target's x and y, similar to how the node's positions are calculated using trigonometry, where x is the rotation angle and y is the radius.
//create the linkRadial function for use later in the 'd' generation
const radialPath = d3.linkRadial()
.angle(l => l.x)
.radius(l => l.y)
const link = svg.append("g")
.attr("fill", "none")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5)
.selectAll("path")
.data(root.links())
.enter()
.append("path")
link.attr("d", function(d){
let adjust = 1.5708 //90 degrees in radians
// calculate the start and end points of the path, using trig
let sourceX = (d.source.y * Math.cos(d.source.x - adjust));
let sourceY = (d.source.y * Math.sin(d.source.x - adjust));
let targetX = (d.target.y * Math.cos(d.target.x - adjust));
let targetY = (d.target.y * Math.sin(d.target.x -adjust));
// if the source node is at the centre, depth = 0, then create a straight path using the L (lineto) SVG path. Else, use the radial path
if (d.source.depth==0){
return "M" + sourceX + " " + sourceY + " "
+ "L" + targetX + " " + targetY
} else {
return radialPath(d)
}
})
I have no idea what is happening here, but when I draw my network diagram, it ends up like this :
Notice the blue lines to the right. I have a zooming ability, and when I zoom, the blue paths on the right disappear.
My code base is huge, so I'll try get a codePen together of an example to see if I can recreate it. But I used this as a guideline for creating curved links :
https://bl.ocks.org/mbostock/4600693
This is when I hit the issue.
Some code for the network creation :
Data
var bilinks = [];
edges.forEach(function (d) {
var s = d.source;
var t = d.target;
var i = {};
edges.push({
source: s,
target: i
}, {
source: i,
target: t
});
nodes.push(i);
bilinks.push({
source: s,
target: t,
middleNode: i
});
});
Path creation :
linkEnter
.append('path')
.attr('id', function (d, i) {
return d.id
})
.attr('class', 'network-path')
.attr('stroke', function (d) {
return colour(d.color);
})
.attr('stroke-width', 1)
.attr('fill', 'none')
.on('click', function (d) {
console.log(d);
})
Perhaps there is a similar question out there, but I'm not sure what to search for.
By the way, thie blue lines on the right are not selectable with the developer selector tool. I'm not sure how it would, but looks similar to when you have a loose monitor connection, I'm really not sure.
Added :
So, I've hidden the nodes, and gone into the elements area. Hovered over the paths you see above, and as you can see, the boundary is only small. When I hide the content in the blue box, the bunch of paths to the right disappear. When I unhide the elements, they return. I can not select the paths to the right via the select tool in dev tools.
EDIT
Tick functionality, drawing the path :
link.selectAll('path').attr('d', function (d) {
// ----
// Total difference in x and y from source to target
var diffX = d.target.x - d.source.x;
var diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
var pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
// x and y distances from center to outside edge of target node
var offsetX = (diffX * nodeSize) / pathLength;
var offsetY = (diffY * nodeSize) / pathLength;
// return "M" + d.source.x + "," + d.source.y + "L" + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
var thisPath = 'M' + d.source.x + ',' + d.source.y +
'S' + d.middleNode.x + ',' + d.middleNode.y +
' ' + (d.target.x - offsetX) + ',' + (d.target.y - offsetY);
return thisPath;
});
Here is a codePen of the Bostock example : https://codepen.io/anon/pen/ePJbKZ
If you drag one of the nodes ontop of the other, you should be able to see the issue.
The problem is the rendering of the Cubic Bezier splines when the points are co-linear.
If you set the d3.forceManyBody() to a strength of -1 the effect is more visible.
It looks like it is a render problem (rounding error) in the erasing of these Cubic Bezier splines. If you drag a node over the ghost lines they disappear because this part of the SVG is re-rendered.
Choosing a different spline type Q or L (straight line) does not have this erase problem.
On our website we draw animated linecharts using d3, created using the following example:
http://bl.ocks.org/duopixel/4063326
Sometimes it happens for some points there's no data, so we updated the chart using the defined method (like this example: https://bl.ocks.org/mbostock/0533f44f2cfabecc5e3a)
The problem we now have is that when using the code for transition:
linePath
.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(2000)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0);
each line of the path is drawn at the same time, as you can see here:
https://jsfiddle.net/applepie89/9kknu8du/
Is there a way/solution to draw it the way it draws the line without missing data? So each "part" follows up the the previous "part"
Based on the comments of Hugues Moreau it does not seem possible to achieve the result with a single path, so I've created a work-around for myself.
At the start there's an array with data, with the null-values in it.
I create a new array and add sub-arrays to this array.
var newObjectArray = [];
var duration = 2000;
var duration_per_point = duration / d.values.length;
var delay = 0;
var start = 0;
for (var j = 0; j < d.values.length; j++)
{
if (isNaN(d.values[j].y))
{
var tempArray = d.values.slice(start, j);
newObjectArray.push(tempArray );
start = j;
}
}
newObjectArray.push(d.values.slice(start, j));
for each array in the newObjectArray I now draw a path, and add delay to the transition.
linePath
.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(duration_per_point*newObjectArray[k].length)
.delay(delay)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0);
delay += duration_per_point * newObjectArray[k].length;
jsfiddle: https://jsfiddle.net/9kknu8du/4/
My problem is simple to explain but I am having real trouble implementing a solution. I am trying to animate a circle along a path on a D3 map. The twist here is that I would like to use one of Mike Bostock's spinny globes (i.e. 3D map).
In time, I would like to add other paths to the globe and to use these for my animations. For now, I would simply like to animate the circles along the border of Russia (i.e. along the path of the Russia polygon coordinates)
I have built a jsfiddle to get traction on this and you can see all my code. Unfortunately I cannot get it to work, and am hoping you can help me out. My jsfiddle: http://jsfiddle.net/Guill84/xqmevpjg/7/
I think my key difficulty is (a) actually referencing the Russia path, and I think I am not getting it right at the moment, and (b) making sure that the interpolation is calculated properly (i.e. that the animation is dynamically linked to the globe, and not just 'layered on top'). The code that is supposed to do that is as follows:
setTimeout(function(){
var path = d3.select("path#Russia"),
startPoint = pathStartPoint(path);
marker.attr("r", 7)
.attr("transform", "translate(" + startPoint + ")");
transition();
//Get path start point for placing marker
function pathStartPoint(path) {
var d = path.attr("d"),
dsplitted = d.split(" ");
return dsplitted[1].split(",");
}
function transition() {
marker.transition()
.duration(7500)
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);// infinite loop
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}
I'd be hugely grateful for any help.
For the first part of your question, one way to select the path is to add an id to id :
d3.json("http://mbostock.github.io/d3/talk/20111018/world-countries.json", function(collection) {
feature = svg.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d", clip)
.attr("id", function(d) { return d.properties.name; }) ;
and then select the path like that :
var path = d3.select("#Russia").node()
Then you can select the first point with :
path.getPointAtLength(0)
See this updated fiddle : http://jsfiddle.net/xqmevpjg/11/
I'm using D3's world-countries.json file to create a mercator map of world countries, which I'll then bind to some data for a non-contiguous cartogram. Alas, the much larger sizes of Canada, the U.S., Australia, etc. mean that one unit for those countries is the spatial equivalent of several units for, say, Malta.
What I think I need to do is normalize the geojson shapes, such that Canada and Malta are the same size when starting out.
Any idea how I'd do that?
Thanks!
Update: I've tried explicitly setting the width and height of all the paths to a small integer, but that seems to just get overridden by the transform later. Code follows:
// Our projection.
var xy = d3.geo.mercator(),
path = d3.geo.path().projection(xy);
var states = d3.select("body")
.append("svg")
.append("g")
.attr("id", "states");
function by_number() {
function compute_by_number(collection, countries) {
//update
var shapes = states
.selectAll("path")
.data(collection.features, function(d){ return d.properties.name; });
//enter
shapes.enter().append("path")
.attr("d", path)
.attr("width", 5) //Trying to set width here; seems to have no effect.
.attr("height", 5) //Trying to set height here; seems to have no effect.
.attr("transform", function(d) { //This works.
var centroid = path.centroid(d),
x = centroid[0],
y = centroid[1];
return "translate(" + x + "," + y + ")"
+ "scale(" + Math.sqrt(countries[d.properties.name] || 0) + ")"
+ "translate(" + -x + "," + -y + ")";
})
.append("title")
.text(function(d) { return d.properties.name; });
//exit
}
d3.text("../data/country_totals.csv", function(csvtext){
var data = d3.csv.parse(csvtext);
var countries = [];
for (var i = 0; i < data.length; i++) {
var countryName = data[i].country.charAt(0).toUpperCase() + data[i].country.slice(1).toLowerCase();
countries[countryName] = data[i].total;
}
if (typeof window.country_json === "undefined") {
d3.json("../data/world-countries.json", function(collection) {
window.country_json = collection;
compute_by_number(collection, countries);
});
} else {
collection = window.country_json;
compute_by_number(collection, countries);
}
});
} //end by_number
by_number();
You might be able to use the helper function I posted here: https://gist.github.com/1756257
This scales a projection to fit a given GeoJSON object into a given bounding box. One advantage of scaling the projection, rather than using a transform to scale the whole path, is that strokes can be consistent across maps.
Another, simpler option might be to:
Project the paths;
Use path.getBBox() to get the bounding box for each (.getBBox() is a native SVG function, not a D3 method)
Set a transform on the path, similar to how you do it now, to scale and translate the path to fit your bounding box.
This is a bit simpler, as it doesn't involve projections, but you'll need to scale the stroke by the inverse (1/scale) to keep them consistent (and therefore you won't be able to set stroke values with CSS). It also requires actually rendering the path first, then scaling it - this might affect performance for complex geometries.