Side-by-side paths in d3 - javascript

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

Related

D3JS making a bar chart where each row/data point contains multiple time segments

I'm trying to make a chart of sleeping sessions that looks something like this:
https://i.stack.imgur.com/Wsnha.png
I currently have only been able to get it to draw one rectangle per data point. I'm wondering what the syntax would be to have it draw multiple rectangles associated with a single row/data point since each data point contains an array of sleeping sessions from that day.
Here is what my code currently looks like:
var start;
var end;
var rectGrp = svg
.append("g")
.attr('transform', 'translate(' +padding.left+','+padding.top+')')
.selectAll("rect")
.data(sleepArr)
.enter()
//I want to create one of these rectangles for all the sleeping sessions in that day
.append("rect")
.attr("x", (d) => {
start = d.sessions[0].startTime;
end = d.sessions[0].endTime;
return xScale(start);
})
.attr("width", (d) => {
start = d.sessions[0].startTime;
end = d.sessions[0].endTime;
var width = end-start;
return xScale(width);
})
.attr("y", (d) => yScale(d.date))
.attr("height", yScale.bandwidth())
.attr("fill", (d) => colors[getDayOfWeek(d.date)])
.append("title")
.text((d) => d.date);
I realize that I probably don't have a good understanding of how to use D3 yet and any help would be greatly appreciated.
With the enter/update/exit cycle one item in the data array should be bound to one element in the DOM. The enter selection is used to ensure we create one element for every item for which there is already not one corresponding element in the DOM. In your case the number of rectangles you want to draw is not the same as the number of people in your data array. The approach you have is great for a flat data structure, but we need to make nested selectAll statements in order to properly represent your data.
First, each item in sleepArr represents one person, so lets create one parent g for every person:
var sleepers = svg
.append("g")
.attr('transform', 'translate(' +padding.left+','+padding.top+')')
.selectAll("rect")
.data(sleepArr)
.enter()
.append("g")
.attr("transform", (d,i)=>...
We probably want to do the vertical positioning here as all children of each parent g elements are the same.
Second, we want to create child rectangles representing sleep sessions. We want one rectangle per session. The data array for these rectangles is the sessions property of the datum bound to each parent g, so we pass that to the data method:
sleepers.selectAll("rect")
.data(function(d) return d.sessions; }))
.enter()
.append("rect")
.attr("x", ...
...
For each parent g, representing a single person, we select all child rectangles of each g, bind data, and enter the required rectangles in the appropriate g.
This way we keep a one to one relationship between data and elements representing them: one g per person, one rect per session. The items in sleepArr are bound to the parent g elements, and the items in the child data array are bound to the rects.

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.

D3 tree level selector

Could I add a level selector for these trees created with d3.js?
http://bl.ocks.org/mbostock/2966094
or
bl.ocks.org/mbostock/4339083
Add a label on each level to get the level position or expand it.
Added a example picture.
Taking the example here: http://bl.ocks.org/mbostock/4339083
I would start by nesting the nodes by level:
var nodesByLevel = d3.nest().key(function (d) {return d.depth}).entries(nodes);
To add your boxes, do something like:
svg.selectAll(".levelBox")
.data(nodesByLevel)
.enter() // one box per level
.append("text")
.attr("class","levelBox")
.attr("x", function (d) {return d.values[0].x}) //take the x of the first node in this layer
.text(function(d) {return d.key}) //the key from the nesting, i.e. the depth
.onclick(levelExpand); // click handler
The above is just a skeleton, that should go into the update function (you need to take care of the exit() and update() selections after adding the data, and any additional drawing features).
In levelExpand, you have access to the list of nodes for the box that is clicked (in d.values). You can then go through the list, expand them, and then update the drawing
function levelExpand(d) {
d.values.forEach(function (n) {n.children = n._children;}); //expand all nodes internally
update(root); //show the update
}

D3 update circle-pack data new nodes overlap existing nodes

I'm following the General Update Pattern but having an issue with regards to layering.
Using a circle-pack layout, I pack the new data, update, enter and exit the circle elements. However, when new elements enter, they overlap the updated circles.
Data key function is based on element name:
.data(nodes, function(d, i) { return d.name; });
So my circle pack has a spot for the updated circle (of the correct location and size) but it's hidden behind its newly entered parent circle.
Is there a way to send these updated nodes to the front or redraw them over the entered circles?
--UPDATE--
As suggested by the person who closed this issue, I've tried implementing the linked to solution using moveToFront.
I added the following code in my update section (which didn't change anything) and then tried adding it after the enter and exit code, which also didn't make any difference.
.each("end", function(d){ d3.select(this).moveToFront(); });
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
For clarity, this is what the selection and update looks like:
// Load data into svg, join new data with old elements, if any.
var nodes = pack.nodes(postData);
node = root = postData;
groupNodes = svg.selectAll("g")
.data(nodes, function(d, i) { return d.name; });
// Update and transition existing elements
groupNodes.select("circle")
.transition()
.duration(duration)
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; })
.attr('r', function(d) { return d.r; })
.each("end", function(d){ d3.select(this).moveToFront(); });
This moveToFront code does not make a difference to my output, and the updated circles remain behind the entered selection circles.
To summarize: the issue seems to be caused by a hierarchy layout (circle-packing) which expects the circles to be drawn in the order of the data's hierarchy. The d3 update pattern (using enter, update and exit selections) causes selected update elements to remain in the svg when the hierarchy is re-drawn, and the new layers are drawn over it. The parents of those nodes are already correctly set, so parentNode.appendChild doesn't do anything in this case, because it's not the cause of the issue.
Here is a fiddle to demonstrate my issue. I've tried putting the moveToFront code in various places, with no visible difference.
When you hit the "Change Data" button, it'll redraw the circles, but any circles whose names overlap between the two data sets are not nested properly in the circle-pack. Children of "Group A" are hidden behind one of the parent circles. You can verify the nodes are there via Inspect Element.
Another pic from the updated fiddle:
D3 provides a way to reorder elements based on the data bound to them with the .sort() function. In your case, the condition to check is the .depth attribute of the elements -- "deeper" elements should appear in front:
svg.selectAll("g")
.sort(function (a, b) {
if (a.depth < b.depth) return -1;
else return 1;
});
Complete demo here.

Unable to correctly redraw in d3.js after removing the first item from the nodes

I'm currently building a d3.js script based on this script. I can get the graph adding new nodes perfectly, but when it comes to removing nodes, it has some trouble.
If I use nodes.pop() to remove the last element, it'll run correctly, but when removing the first element using nodes.shift(), the nodes are redrawn incorrectly. For example, if 4 nodes are added, so that the node array becomes:
[0] = color.orange
[1] = color.blue
[2] = color.green
[3] = color.green
then nodes.shift() is called, the first element is moved correctly, so that the array becomes:
[0] = color.blue
[1] = color.green
[2] = color.green
When being drawn on screen though, node[0], which is now blue, should remain in the same location, but what actually happens is that it moves to where the orange circles are being stored. The Cx and Cy value of the blue circle don't change (the centre for all nodes of that colour) so I'm not really sure what's causing this. If I call removeNode again, the elements will be shifted correctly, but the item now in index 1 will move to where the blue circles are being drawn. I thought that this may be an issue with the node array being used to redraw while the elements were still being shifted, so I used a timeout with a redraw callback, but this didn't work unfortunately. I thought that the id of nodes may need to be decreased to match their index in the array, but this too didn't work.
The script can be found here, although nothing will display at first. Using the console, call addNode(); several times to see how the script runs.
I realised as I was writing this what I had been doing wrong. Following this tutorial showed me that I needed to call circle = circle.data(force.nodes(), function(d) { return d.id;}); at the top of redraw, so that redraw now looks like
var circle = svg.selectAll("circle");
circle = circle.data(force.nodes(), function(d) { return d.id;});
circle
.enter().append("circle")
.attr("cx", function(d) { return d.cx; })
.attr("cy", function(d) { return d.cy; })
.attr("r", function(d) { return d.radius; })
.style("fill", function(d) { return d.color; });
circle.exit().remove();
force.start();

Categories