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

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]

Related

how to highlight max and min points on lineChart

I use dc.js lineChart and barChart. Now I need to mark the maximum and minimum values on my lineChart with 'renderArea(true)'.
I want something like in the picture below or maybe something else, but I don't know how to add this feature.
Update:
Gordon's answer is perfect. Unfortunately, my chart doesn't show the hint with 'mouseover' on marked points
One more update:
How can I redraw these points after zooming?
This isn't something supported directly by dc.js, but you can annotate the chart with a renderlet. Gladly, dc.js makes it easy to escape out to d3 when you need custom annotations like this.
We'll use the fact that by default the line chart draws invisible dots at each data point (which only appear when they are hovered over). We'll grab the coordinates from those and use them to draw or update our own dots in another layer.
Usually we'd want to use a pretransition event handler, but those dots don't seem to have positions until after the transition, so we'll have to handle the renderlet event instead:
chart.on('renderlet', function(chart) { // 1
// create a layer for the highlights, only once
// insert it after the tooltip/dots layer
var highlightLayer = chart.select('g.chart-body') // 2
.selectAll('g.highlight-dots').data([0]);
highlightLayer
.enter().insert('g', 'g.dc-tooltip-list').attr('class', 'highlight-dots');
chart.selectAll('g.dc-tooltip').each(function(_, stacki) { // 3
var dots = d3.select(this).selectAll('circle.dot'); // 4
var data = dots.data();
var mini = 0, maxi = 0;
data.forEach(function(d, i) { // 5
if(i===0) return;
if(d.y < data[mini].y)
mini = i;
if(d.y > data[maxi].y)
maxi = i;
});
var highlightData = [mini, maxi].map(function(i) { // 6
var dot = dots.filter(function(_, j) { return j === i; });
return {
x: dot.attr('cx'),
y: dot.attr('cy'),
color: dot.attr('fill')
}
});
var highlights = highlightLayer.selectAll('circle.minmax-highlight._' + stacki).data(highlightData);
highlights
.enter().append('circle') // 7
.attr({
class: 'minmax-highlight _' + stacki,
r: 10,
'fill-opacity': 0.2,
'stroke-opacity': 0.8
});
highlights.attr({ // 8
cx: function(d) { return d.x; },
cy: function(d) { return d.y; },
stroke: function(d) { return d.color; },
fill: function(d) { return d.color; }
});
});
});
This is fairly complicated, so let's look at it step-by-step:
We're listening for the renderlet event, which fires after everything has transitioned
We'll create another layer. The .data([0]).enter().insert(stuff) trick is a degenerate case of the d3 general update pattern that just makes sure an item is added exactly once. We specify the selector for the existing tooltip/dots layer as the second parameter to .insert(), in order to put this layer before in DOM order, which means behind. Also, we'll hold onto the update selection because that is either the inserted node or the existing node.
We iterate through each of the stacks of tooltip-dots
In each stack, we'll select all the existing dots,
and iterate over all their data, finding the minimum and maximum indices mini and maxi.
Now we'll create a two-element data array for binding to the min/max highlight dots, pulling data from the existing dots
Now we're finally ready to draw stuff. We'll use the same degenerate update pattern to draw two dots with class minmax-highlight _1, _2, etc.
And use the color and positions that we remembered in step 6
Note that the min and max for each stack is not necessarily the same as the total min and max, so the highlighted points for a higher stack might not be the highest or lowest points.
Not so simple, but not too hard if you're willing to do some d3 hacking.
Example fiddle: http://jsfiddle.net/gordonwoodhull/7vptdou5/31/

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.

Add a vertex in the middle of a path with d3js

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.

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.

Third variable in D3 anonymous function

Let's say you've got a selection with some data bound to it and you use the typical inline anonymous function to access that data:
d3.select("#whatever").each(function(d,i,q) {console.log(d,i,q)})
We all know the first variable is the data and the second is the array position. But what does the third variable (q in this case) represent? So far it's always come back zero in everything I've tested.
The secret third argument is only of use when you have nested selections. In these cases, it holds the index of the parent data element. Consider for example this code.
var sel = d3.selectAll("foo")
.data(data)
.enter()
.append("foo");
var subsel = sel.selectAll("bar")
.data(function(d) { return d; })
.enter()
.append("bar");
Assuming that data is a nested structure, you can now do this.
subsel.attr("foobar", function(d, i) { console.log(d, i); });
This, unsurprisingly, will log the data item inside the nesting and its index. But you can also do this.
subsel.attr("foobar", function(d, i, j) { console.log(d, i, j); });
Here d and i still refer to the same things, but j refers to the index of the parent data element, i.e. the index of the foo element.
A note on Lars's reply, which is correct but I found one more feature that is helpful.
The j element gives the index of the element without regard to the nesting of the parent elements. In other words, if you are appending and logging as follows, the final circles are treated as a flat array, not as a group of nested arrays. So your indexes will be scaled from 0 to the number of circle elements you have, without regard to the data structure of your nesting.
var categorygroups = chart.selectAll('g.categorygroups')
.data(data)
.enter()
.append('g').attr('class','categorygroups');
var valuesgroups = categorygroups.selectAll('g.valuesgroups')
.data(function(d) {return d.values; }).enter().append('g').attr('class','valuesgroups');
valuesgroups.append('text').text(function(d) {
return d.category
}).attr('y',function(d,i) { return (i + 1) * 100 }).attr('x',0);
var circlesgroups = valuesgroups.selectAll('g.circlesgroups')
.data(function(d) {return d.values; }).enter().append('g').attr('class','circlesgroups');
circlesgroups.append('circle').style('fill','#666')
.attr('cy',function(d,i,j) { console.log(j); return (j + 1) * 100 })
.attr('cx',function(d,i) { return (i + 1) * 40 });

Resources