Add a vertex in the middle of a path with d3js - d3.js

How can I modify a path according to user actions?
For example: I have a path composed of three points A, B and C. When the user clicks on the path (somewhere other than an existing point) I would like add a new point at that position to the path. How can I insert the new point into the path at the correct location?
Here you can find an example
var nodes = [[30, 130], [250, 250], [400,130]];
var line = d3.svg.line();
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 5000);
var path = svg.append("path")
.datum(nodes)
.attr("class", "line")
.call(update);
path.on("click", insertNode);
function update() {
svg.select("path").attr("d", line);
var circle = svg.selectAll("circle")
.data(nodes, function(d) { return d; });
}
function insertNode(data) {
//create the new node:
var newNode = [];
var newNode = d3.mouse(svg.node());
//find coordinates relative to the plotting region
nodes.push(newNode); //add to your nodes array
update();
}
If you click on the first segment (sx -> dx) a new segment is added from the end of the path to the new node because I add the new node at the end of the nodes array.The right behavior is a new node in the path (nodes array) between the nodes [30, 130] and [250, 250]
Thanks!
aGO!

You can't simply add the new node to the array, you have to determine its position first. One way of doing this is to compute the angle to all points. When the absolute values are the same, you know that you've found your insert position. The only snag is that because of the width of the line, it won't be exactly 180 degrees, so you have to account for that. The following code attempts this and splices the new node into the array.
var idx = 0, prevAngle;
nodes.forEach(function(n, i) {
var angle = Math.abs(Math.atan((n[1] - newNode[1]) / (n[0] - newNode[0])));
if(Math.abs(angle - prevAngle) < 0.05) {
idx = i;
}
prevAngle = angle;
});
tmp = nodes.slice(0, idx);
tmp.push(newNode);
nodes = tmp.concat(nodes.slice(idx));
Complete example here.

The link's data object contains the source and target node data objects, so you can use that information to split the link in two, connected through the new node.
Sample code, assuming linkElements is your d3 selection of the link <path> or <line> elements, and links and nodes are the data arrays corresponding to force.links() and force.nodes():
linkElements.on("click", insertNode);
function insertNode(linkData){ //parameter is the *link's* data object
//create the new node:
var newNode = {};
var clickPoint = d3.mouse(this.parentNode);
//find coordinates relative to the plotting region
newNode.x = xScale.invert(clickPoint[0]);
newNode.y = yScale.invert(clickPoint[1]);
//convert coordinates to data values
nodes.push(newNode); //add to your nodes array
//create a new link for the second half of the old link:
var newLink = {source:newNode, target:linkData.target};
links.push(newLink); //add to your links array
//update the old link to point to the new node:
linkData.target = newNode;
update();
}
This of course only updates the data objects for the nodes and links, your update() function will have to update the force layout and create the entering SVG elements.

Related

D3 v4 (Tree Diagram) - Dynamically calculate the height of the SVG given dynamic number of nodes

I dynamically am changing the data for a Tree Diagram, and don't know what the height of the SVG should be. How can I calculate the height of the Tree Diagram to update the SVG dimensions?
Fiddle example: https://jsfiddle.net/intelligence_ai/guesukv6/
Please view the code on the fiddle, SO requires that I post some code with fiddle links:
var treemap = d3.tree().nodeSize([40, 40]);
root = d3.hierarchy(data, function(d) { return d.children; });
root.x0 = 100;
root.y0 = 0;
function update(source) {
// Assigns the x and y position for the nodes
var data = treemap(root);
// Compute the new tree layout.
var nodes = data.descendants(),
links = data.descendants().slice(1);
// Normalize for fixed-depth.
let connectorLength = 200; // the length of the lines in pixels
nodes.forEach(function(d){ d.y = d.depth * connectorLength});
nodes = nodes.filter(function(d){
return d.depth != 0;
})
...
Since you specified that the nodeSize is [40,40] and you have 129 nodes, the height should be:
var height = 40 * 129;
Since you want to calculate it dynamically, it should be:
var height = treemap.nodeSize()[1] * nodes.length;
Also, when specifying tree.nodeSize the root node will be positioned at 0,0. Therefore, you have to translate the main group:
.attr("transform", "translate("+ margin.left + "," + (height/2) + ")");
Here is the updated fiddle: https://jsfiddle.net/jxw0Ld3c/

Difficulty understanding d3 version 4 selection life cycle with nested elements

In version d3 v3, a common workflow for me was to create several svg group elements and then append a number of other child elements to each g. as an example, below I've created 3 group elements and appended a circle to each group. I then use the selection.each method to update the radius of each circle:
var data = [2, 4, 8]
var g = d3.select('svg').selectAll('.g').data(data)
g.each(function(datum) {
var thisG = d3.select(this)
var circle = thisG.selectAll('.circle')
circle.transition().attr('r', datum * 2)
})
var enterG = g.enter().append('g').attr('class', 'g')
enterG.append('circle')
.attr('class', 'circle')
.attr('r', function(d) { return d })
g.exit().remove()
What is the proper way to do this in d3 v4? I am very confused on how best to do this. Here's an example of what i'm trying:
var data = [2, 4, 8]
var g = d3.select('svg').selectAll('.g').data(data)
g.enter()
// do stuff to the entering group
.append('g')
.attr('class', 'g')
// do stuff to the entering AND updating group
.merge(g)
// why do i need to reselect all groups here to append additional elements?
// is it because selections are now immutable?
var g = d3.select('svg').selectAll('g')
g.append('circle')
.attr('class', 'circle')
.attr('r', function(d) { return d })
// for each of the enter and updated groups, adjust the radius of the child circles
g.each(function(datum) {
var thisG = d3.select(this)
var circle = thisG.selectAll('.circle')
circle.transition().attr('r', datum * 2)
})
g.exit().remove()
Thanks in advance for any help you can provide. I've used d3 v3 for a long time and feel pretty comfortable with it. However, I am having a very hard time understanding some of the different behaviors in v4.
I think your code could be modified as follow (untested, so unsure):
var data = [2, 4, 8]
var g = d3.select('svg').selectAll('.g').data(data);
// do stuff to the entering group
var enterSelection = g.enter();
var enterG = enterSelection.append('g')
.attr('class', 'g');
//Append circles only to new elements
enterG.append('circle')
.attr('class', 'circle')
.attr('r', function(d) { return d })
// for each of the enter and updated groups, adjust the radius of the child circles
enterG.merge(g)
.select('.circle')
.transition()
.attr('r',function(d){return d*2});
g.exit().remove()
When using the first .selectAll, only existing elements are selected. Then, by entering, you are creating new elements, that generate a new selection. When you need to update all, you simply merge the new and existing elements in a single selection.
From that selection, I simply selected all .circle (single select - one element per g), and then update the radius thanks to the binding API that prevents me from making a .each call. I am unsure as how these two compares, I simply always did it this way.
Finally, here is a bl.ocks demonstrating the pattern.

Violin plot in d3

I need to build a violin point with discrete data points in d3.
Example:
I am not sure how to align the center for each value on X axis. The default behavior will overlay all the points with same X and Y value, however I would like the points to be offset while being center aligned e.g. 5.1 has 3 values in control group and 4.5 has 2 values, all center aligned. It is easy to do so for either right or left aligned by doing a transformation of each point by a specified amount. However, the center alignment seems to be quite hacky.
A hacky way would be to manually transform the X value by maintaining a couple of arrays to see whether this is the first, even or odd number of element and place it according my specifying the value. Is there a proper way to handle this?
The only example of violin plot in d3 I found was here - which implements a probability distribution rather than the discrete values which I require.
"A hacky way would be to manually transform the X value by maintaining a couple of arrays" - that's pretty much the way most d3 layouts work :-) . Discretise your data set by the y value (weight), keeping a total of the data points in each discrete group and a group index for each datum. Then use those to calculate offsets x-ways and the rounded y-value.
See https://jsfiddle.net/n444k759/4/
// below code assumes a svg and g group element are present (they are in the jsfiddle)
var yscale = d3.scale.linear().domain([0,10]).range([0,390]);
var xscale = d3.scale.linear().domain([0,2]).range ([0,390])
var color = d3.scale.ordinal().domain([0,1]).range(["red", "blue"]);
var data = [];
for (var n = 0; n <100; n++) {
data.push({weight: Math.random() * 10.0, category: Math.floor (Math.random() * 2.0)});
}
var groups = {};
var circleR = 5;
var discreteTo = (circleR * 2) / (yscale.range()[1] / yscale.domain()[1]);
data.forEach (function(datum) {
var g = Math.floor (datum.weight / discreteTo);
var cat = datum.category;
var ref = cat+"-"+g;
if (!groups[ref]) { groups[ref] = 0; }
datum.groupIndex = groups[ref];
datum.discy = yscale (g * discreteTo); // discrete
groups[ref]++;
});
data.forEach (function(datum) {
var cat = datum.category;
var g = Math.floor (datum.weight / discreteTo);
var ref = cat+"-"+g;
datum.offset = datum.groupIndex - ((groups[ref] - 1) / 2);
});
d3.select("svg g").selectAll("circle").data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return 50 + xscale(d.category) + (d.offset * (circleR * 2)); })
.attr("r", circleR)
.attr("cy", function(d) { return 10 + d.discy; })
.style ("fill", function(d) { return color(d.category); })
;
The above example discretes into groups according to the size of the display and the size of the circle to display. You might want to discrete by a given interval and then work out the size of circle from that.
Edit: Updated to show how to differentiate when category is different as in your screenshot above

Avoid overlapping of nodes in tree layout in d3.js

I have created a collapsible tree to represent some biological data.
In this tree the size of the node represents the importance of the node. As I have a huge data and also the sizes of the nodes vary,they overlap over each other. I need to specify the distance between the sibling nodes.
I tried tree.separation() method but it didn't work.
Code is as follows :
tree.separation(seperator);
function seperator(a, b)
{
if(a.parent == b.parent)
{
if((a.abundance == (maxAbd)) || (b.abundance == (maxAbd)))
{
return 2;
}
else
{
return 1;
}
}
}
This is giving me error saying:
Unexpected value translate(433.33333333333337,NaN) parsing transform attribute.
I understand that that after adding the separation method it is unable to calculate the x coordinate for the nodes. Can anyone please help me with how to do this?
I also tried modifying the source code as suggested in https://groups.google.com/forum/#!topic/d3-js/7Js0dGrnyek but that did not work either.
Please suggest some solution.
I had the same problem. This is how I solved it. I have width assigned to each node, height for now is the same for all nodes (basically nodes with smaller height than nodeHeight, get centered vertically):
var tree = d3.layout.tree().nodeSize([1, nodeHeight])
.separation(function(a, b) {
var width = a.width + b.width,
distance = width / 2 + 16; // horizontal distance between nodes = 16
return distance;
}),
nodes = tree.nodes(data),
links = tree.links(nodes);
Hope this helps.
SiegS's answer just works fine!
My situation is that: My node is actually some text, which may have various width, which I don't know in advance. So I need to calculate the width of each nodes first.
I have a JSON object json_data as my data.
var tree = d3.layout.tree()
.sort(null)
.size([500,500])
.children( some_function_identify_children );
var nodes = tree.nodes(json_data); //which doesn't consider the node's size;
var links = tree.links(nodes);
// append a svg;
var layoutRoot = d3.select("body")
.append("svg:svg").attr("width","600").attr("height","600")
.append("svg:g")
.attr("class","container");
var nodeGroup = layoutRoot.selectAll("g.node")
.data(nodes)
.enter().append("text").text(function(d){return d.text;});
// now we knows the text of each node.
//calculate each nodes's width by getBBox();
nodeGroup.each(function(d,i){d["width"] = this.getBBox().width;})
//set up a new tree layout which consider the node width.
var newtree = d3.layout.tree()
.size([500,500])
.children(function(d) {return d.children;})
.separation(function(a,b){
return (a.width+b.width)/2+2;
});
//recalcuate the node's x and y by newtree
newtree.nodes(nodes[0]); //nodes[0] is the root
links = newtree.links(nodes);
//redraw the svg using new nodes and links.
...
Hope this will help.

D3.js: Appending different amount of elements to group?

I'm trying to create a graph representation but I'm not sure how to organize my data.
I currently have all the data bound to a group and have drawn the nodes.
The data includes the list of edges but I'm not sure how to draw them as each node has a different number of edges.
var group = svg.selectAll("g").data(dataset).enter().append("g);
//this works
group.append("circle")
.attr(...).attr(...).attr(...);
//what to do here? when I call group, it will loop through each node ONCE and append a path
//but I need it to append edges.length paths for each pass
group.append("path")
.attr("d", function(d,i){
//example of 1 line
lineSeg = [];
lineSeg.push({"x":d.x ,"y":d.y});
lineSeg.push({"x":d.edges[1].x ,"y":d.edges[1].y});
return line(lineSeg);
});
edit:
//just to add dom structure, my goal is for it to be:
g:
circle
path1
path2
g:
circle
path1
path2
path3
g:
circle
path1
etc...
Lars linked to the relevant tutorial page. The key for me was the nested data joins
var td = d3.selectAll("tbody tr")
.data(matrix)
.selectAll("td")
.data(function(d, i) { return d; }); // d is matrix[i]

Resources