D3.js - animate point along path w/ variable speed - javascript

I have three arrays of points that define three paths, and need to move a different point along each path w/ the option of changing speed. The three arrays are contained in another array, so the structure basically looks like this:
DataByDays = [ [(array of x1's), (array of y1's)], [(array of x2's), (array of y2's)], [(array of x3's), (array of y3's)]
I have already plotted the paths and have an array that holds them similarly, like:
PathArray = [path1, path2, path3]
Currently, I've created each of the trackers:
trackers = svg.selectAll("circle").data(dataByDays)
.enter()
.append("circle")
.attr("cx", function(d){return d[0][0];}) //x coord # start of path
.attr("cy", function(d){return d[0][1];}) //y coord # start of path
.attr("r", 5)
.attr("fill", "black");
I've tried to adapt a variety of solutions to similar problems but nothing has worked. I'd like to start with just getting the trackers to move along the path, but also need to incorporate the ability to change the speed at which the trackers move with a slider that I've already created.

Related

How to add and remove nodes with nested groups inside a D3 force layout?

I have a D3 force layout that updates the number of nodes several times per second and that is called by restartD3(). I currently have a circle appended to each node and that works great. However, I now need to have 2 circles per node, but the key here is that they need to be on unique layers by type not by node, so I need to put them in groups. Here is the grouping layering that I am talking about: link
I want this.circleNode to become an outer group for 2 other groups inside so that I only have to manipulate x and y position for the outer group and the 2 inner groups move as well. Here is my current code with just 1 outer group for the nodes but no groups nested inside yet:
// Create force simulation
this.force = d3.forceSimulation(this.users)
.alphaDecay(0)
.velocityDecay(0)
.on('tick', this.tickActions);
// Create circle nodes
this.circleNode = this.d3Graph.selectAll(null)
.enter()
.append("g")
// Call our restartD3 function
this.restartD3();
// My restartD3 function
restartD3() {
// Circles
this.circleNode = this.circleNode.data(this.users, function(d) {
return d.id;
});
this.circleNode.exit().remove();
this.circleNode = this.circleNode
.enter()
.append("circle")
.attr("class", "usercircles")
.attr("r", this.userRadius)
.attr("fill", d => "#00aced")
.merge(this.circleNode)
this.force.nodes(this.users);
}
I haven't been able to implement this, and my attempts have been far off with nothing rendering. Any guidance on nesting groups in a node that works with updating a lot would be much appreciated.

Importing JSON for D3 map manipulation

tl;dr
I'm creating arcs between two points on a map as shown here, but I want to save that huge array of coordinates in a json/csv file. How should I save that file and what should I change in the script so it correctly parses the json/csv file.
Long version
I'm trying to draw arcs between two points on a map as shown here.
Here's what I did.
First I defined my coordinates (hard-coded). Notice that they are lon/lat.
var trainRoutes = [
{sourceLocation: [94.91542,27.485983],targetLocation: [77.549934,8.079252]}
];
Then I defined my arcs.
var arcs = svg.append("g").attr("class","arcs"); // adding a class for CSS stuff
And finally the code for drawing them.
arcs.selectAll("path")
.data(trainRoutes)
.enter()
.append("path")
.attr('d', function(d) {
return makeArc(d, 'sourceLocation', 'targetLocation', 1);
});
makeArc is just a function that returns a string for the path to be drawn, again, as shown here.
As you can see, I'm just creating one arc with two sets of coordinates (say city A and city B). I would like to draw more arcs but not clutter my index.html. I want to put the coordinates in a JSON file and create arcs from there rather than declaring coordinates within index.html.
I did try putting the coordinates in a JSON file and used d3.json to do the same.
d3.json("trainRoutes.json", function(json){
arcs.selectAll("path")
.data(json)
.enter()
.append("path")
.attr('d', function(d) {
return makeArc(d, d.sourceLocation, d.targetLocation, 1); });
});
But this didn't work and the console says arcs.selectAll("path") is not a function. How do I solve this issue? I'm open to using both d3.csv/d3.json, just want to move the coordinates to another file.
Here's what my JSON file (trainRoutes.json) looked like in case my declaration of JSON was wrong.
[
{sourceLocation: [94.91542,27.485983],targetLocation: [77.549934,8.079252]}
]

Side-by-side paths in d3

I'm trying out a way to get paths to display next to each other, such that they'll push each other around (factoring in widths and neighbouring points) and not overlap.
This is my fiddle, mostly pieced together from examples
https://jsfiddle.net/crimsonbinome22/k2xqn24x/
var LineGroup = svg.append("g")
.attr("class","line");
var line = d3.svg.line()
.interpolate("linear")
.x(function(d) { return (d.x); })
.y(function(d) { return (d.y); })
;
LineGroup.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr("d", function(d){ return line(d.p); })
.attr("stroke", function(d){ return d.c; })
.attr("stroke-width", function(d){ return d.w; })
.attr("fill", "none");
And this is what I'm hoping to achieve in this image here, basically:
For all lines landing on the same point, push them left or right of that point so together they center around it.
Factor in line width so they don't overlap, or leave whitespace between.
Be able to handle paths with different numbers of points (max in example is 3 but I want to deal with up to 10)
Note though points that overlap will always have the same index (they won't loop around, but just go outwards like a tree)
Be able to handle different numbers of lines landing on the same point.
Some issues I'm having:
I'm new to d3 and I find functions a bit baffling. Not sure how to even start to apply logic that will move the lines around.
My data structure has some redundant info in it, such as r for the rank (to decide whether to push left or right) and w for the width both of which will always be the same for a particular line.
I have a lot of data so the data structure used here won't work with the csv data I have. Can maybe skip this one for now and I'll open up a new question for that one later.
I've had a search around but can't find any examples of how to do this. In a way it's almost like a chord diagram but a little different, and I can't find much relevant code to reuse. Any help on how to achieve this (either with the approach I've started, or something totally different if I've missed it) would be appreciated.
I would go with the following steps:
compute an array of node objects, i.e. one object for each point visited by a line
compute the tree on this node (that is, for every node, add links to its parent and children)
make sure that children of any node are ordered according to the angle they make with this node
at this point, each line now only depends on its final node
for each node compute an ordered list of lines going through
visit all nodes bottom-up (i.e. starting from the leaves)
the "go-through" list is the concatenation of the lists of the children + all lines that end at the current node
for each node, compute an array of offsets (by summing the successive width
of the lines going through)
finally, for every line and every node in the line, check the array of offsets to know how much the line must be shifted
Edit: running example
https://jsfiddle.net/toh7d9tq/1/
I have used a slightly different approach for the last two steps (computing the offset): I actually create a new p array for each series with a list of pairs {node, offset}. This way it is much easier to access all relevant data in the drawing function.
I needed to add an artificial root to have a nice starting line (and to make it easier for recursion and angles and everything), you can skip it in the drawing phase if you want.
function key(p) {
return p.time+"_"+p.value
}
// a node has fields:
// - time/value (coordinates)
// - series (set of series going through)
// - parent/children (tree structure)
// - direction: angle of the arc coming from the parent
//artificial root
var root={time:200, value:height, series:[], direction:-Math.PI/2};
//set of nodes
var nodes = d3.map([root], key);
//create nodes, link each series to the corresponding leaf
series.forEach(function(s){
s.pWithOffset=[]; //this will be filled later on
var parent=root;
s.p.forEach(function(d) {
var n=nodes.get(key(d));
if (!n) {
//create node at given coordinates if does not exist
n={time:d.time,
value:d.value,
parent:parent,
series:[],
direction:Math.atan2(d.value-parent.value, d.time-parent.time)};
nodes.set(key(n),n);
//add node to the parent's children
if (!parent.children) parent.children=[];
parent.children.push(n);
}
//this node is the parent of the next one
parent=n;
})
//last node is the leaf of this series
s.leafNode=parent;
parent.series.push(s);
})
//sort children by direction
nodes.values().forEach(function(n){
if (n.children)
n.children.sort(function (a,b){
if (a.direction>n.direction)
return a.direction-b.direction;
});
});
//recursively list all series through each node (bottom-up)
function listSeries(n) {
if (!n.children) return;
n.children.forEach(listSeries);
n.series=d3.merge(n.children.map(function(c){return c.series}));
}
listSeries(root);
//compute offsets for each series in each node, and add them as a list to the corresponding series
//in a first time, this is not centered
function listOffsets(n) {
var offset=0;
n.series.forEach(function(s){
s.pWithOffset.push( {node:n, offset:offset+s.w/2})
offset+=s.w;
})
n.totalOffset=offset;
if (n.children)
n.children.forEach(listOffsets);
}
listOffsets(root);
And then in the drawing section:
var line = d3.svg.line()
.interpolate("linear")
.x(function(d) { return (d.node.time-Math.sin(d.node.direction)*(d.offset-d.node.totalOffset/2)); })
.y(function(d) { return (d.node.value+Math.cos(d.node.direction)*(d.offset-d.node.totalOffset/2)); })
;
LineGroup.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr("d", function(d){ return line(d.pWithOffset); })
.attr("stroke", function(d){ return d.c; })
.attr("stroke-width", function(d){ return d.w; })
.attr("fill", "none");

Jittering geo paths using D3.js

I'm trying to add 'jitter' or add random noise to a D3.js map that contains line features. Note, this is slightly different from this other example because it involves geo paths. Additionally, while I'd like to use a custom transformation to do this, I don't think I can because I need to be able to use a standard transformation (from WGS84 to NY State Plane). I think the jittering function should either be based on a modified path function, or be a separate function which takes a path as input.
var projection = d3.geo.conicConformal()
.parallels([40 + 40 / 60, 41 + 2 / 60])
.rotate([74, -40 - 10 / 60]);
var path = d3.geo.path()
.projection(projection);
Note that I don't really want to modify the input data at all (i.e., the jittering should be on the paths, not the input geodata). Note also that the jittering can be totally random (i.e., it does not have to be the same every time). My initial thought is to wrap the data in a jitter function, or to wrap the path function in a jitter function. Either way, I'm not really sure where to start on this? Any suggestions? Even a link to the relevant API item would be awesome!
svg.selectAll("path")
.data(jitter(lines.features)) // Wrap data in jitter function... or...
.enter().append("path")
.attr("class", "line")
.attr("d", function(d) { return jitter(path(d)); }) // Jitter path directly
A (simplified) jsfiddle is available here for reference.

Improving D3 Sequence Sunburst Example

This D3 example served as my starting point:
http://bl.ocks.org/kerryrodden/7090426
I wanted to change data that feeds the diagram, and I made following new example:
http://jsfiddle.net/ZGVK3/
One can notice at least two problems:
Legend is wrong. This is because it still contains 'hardcoded' names from original example.
All nodes are colored black. This is because the color scheme is also 'hardcoded' only for node names from original example.
How to improve the original example (or my jsfiddle, it doesn't matter) so that legend and coloring are self-adjusted to the data that feeds the diagram?
You can use an ordinal scale to map colors to the different node names. Implementing it would only require a few minor changes to your existing code.
Step 1. Create an ordinal scale for the colors
Instead of having colors be simply a list of color names, hard-coded to specific names, use d3.scale.ordinal(), and set the .range() to be an array of the colors you want to use. For example:
var colors = d3.scale.ordinal()
.range(["#5687d1","#7b615c","#de783b","#6ab975","#a173d1","#bbbbbb"]);
This would create an ordinal scale that uses the same colors as the original visualization. Since your data would require more colors, you would want to add a few more to your range, otherwise colors will be repeated.
As a shortcut, you can use d3.scale.category20() to let d3 choose a range 20 categorical colors for you.
Now when setting the fill colors for your path element arcs and also your breadcrumbs, you would simply use colors(d.name) instead of colors[d.name].
Step 2. Use your data to construct the domain of the scale
The .domain() of this scale will be set once we have the data, since it will depend on a list of the unique names contained in the data. To do this, we can loop through the data, and create an array of the unique names. There are probably several ways to do this, but here's one that works well:
var uniqueNames = (function(a) {
var output = [];
a.forEach(function(d) {
if (output.indexOf(d.name) === -1) {
output.push(d.name);
}
});
return output;
})(nodes);
This creates an empty array, then loops through each element of the nodes array and if the node's name doesn't already exist in the new array, it is added.
Then you can simply set the new array to be the domain of the color scale:
colors.domain(uniqueNames);
Step 3. Use the scale's domain to build the legend
Since the legend is going to depend on the domain, make sure the drawLegend() function is called after the domain is set.
You can find the number of elements in the domain (for setting the height of the legend) by calling colors.domain().length. Then for the legend's .data(), you can use the domain itself. Finally, to set the fill color for the legend boxes, you call the color scale on d since each element in the domain is a name. Here's what those three changes to the legend look like in practice:
var legend = d3.select("#legend").append("svg:svg")
.attr("width", li.w)
.attr("height", colors.domain().length * (li.h + li.s));
var g = legend.selectAll("g")
.data(colors.domain())
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return colors(d); });
And that's about it. Hope that helps.
Here's the updated JSFiddle.

Categories