How to add compound node in a D3 force layout? - javascript

I'm adding nodes to a force layout graph like this:
var node = vis.selectAll("circle.node")
.data(nodes)
.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.style("fill", function(d) { return fill(d.group); })
.call(force.drag);
Is there a way to add compound SVG elements as nodes? I.e. I want to add a hyperlink for each circle, so I'd need something like this:
<circle ...></circle>

Creating a "compound" element is as simple as appending one or more children to another element. In your example, you want to bind your data to a selection of <a> elements, and give each <a> a single <circle> child.
First of all, you need to select "a.node" instead of "circle.node". This is because your hyperlinks are going to be the parent elements. If there isn't an obvious parent element, and you just want to add multiple elements for each datum, use <g>, SVG's group element.
Then, you want to append one <a> element to each node in the entering selection. This creates your hyperlinks. After setting each hyperlink's attributes, you want to give it a <circle> child. Simple: just call .append("circle").
var node = vis.selectAll("a.node")
.data(nodes);
// The entering selection: create the new <a> elements here.
// These elements are automatically part of the update selection in "node".
var nodeEnter = node.enter().append("a")
.attr("class", "node")
.attr("xlink:href", "http://whatever.com")
.call(force.drag);
// Appends a new <circle> element to each element in nodeEnter.
nodeEnter.append("circle")
.attr("r", 5)
.style("fill", function(d) { return fill(d.group); })
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
Remember that D3 primarily operates on selections of nodes. So calling .append() on the entering selection means that each node in the selection gets a new child. Powerful stuff!
One more thing: SVG has its own <a> element, which is what I was referring to above. This is different from the HTML one! Typically, you only use SVG elements with SVG, and HTML with HTML.
Thanks to #mbostock for suggesting that I clarify the variable naming.

Reply to Jason Davies (since stackoverflow limits the length of reply comments…): Excellent answer. Be careful with the method chaining, though; typically you want node to refer to the outer anchor element rather than the inner circle element. So I'd recommend a small variation:
var node = vis.selectAll("a.node")
.data(nodes)
.enter().append("a")
.attr("class", "node")
.attr("xlink:href", "http://whatever.com")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.call(force.drag);
node.append("circle")
.attr("r", 5)
.style("fill", function(d) { return fill(d.group); });
I've also replaced the circle's cx and cy attributes with a transform on the containing anchor element; either one will work. You can treat svg:a elements as svg:g (both are containers), which is nice if you want to add labels later.

Related

Why is my d3 path not transitioning?

My code looks like the following (omitting the scales that I'm using):
// Nesting data attaching "name" as data key.
var dataNest = d3.nest()
.key(function(d) { return d.name; })
.entries(data);
// Defining what the transition for the path.
var baseTransition = d3.transition()
.duration(1000)
.ease(d3.easeLinear);
// Bind data to SVG (use name as key).
var state = svg2
.selectAll(".line2")
.data(dataNest, function(d, i) { return d.key; });
// Enter data join.
state.enter()
.append("path")
.attr("class", "line2");
// Set transition and define new path being transition to.
state.transition(baseTransition)
.style("stroke", "#000")
.style("fill", "none")
.attr("id", function(d) { return 'tag'+d.key.replace(/\s+/g, ''); })
.attr("d", function(d) { return line(d.values); });
state.exit().remove();
I'm mostly following this example in which the transitions are working on top of a drop down list. However, while the code I have above displays paths, it does not transition between the paths. Are there any obvious flaws in my approach?
EDIT: I have a more concrete example of what I want to do with this JSFiddle. I want to transition from data to data2 but the code immediately renders data2.

How to update the scattergraph?

I have this table and chart with scattergraph:
https://jsfiddle.net/horacebury/bygscx8b/6/
And I'm trying to update the positions of the scatter dots when the values in the second table column change.
Based on this SO I thought I could just use a single line (as I'm not changing the number of points, just their positions):
https://stackoverflow.com/a/16071155/71376
However, this code:
svg.selectAll("circle")
.data(data)
.transition()
.duration(1000)
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
});
Is giving me this error:
Uncaught TypeError: svg.selectAll(...).data is not a function
The primary issue is that:
svg.selectAll("circle") is not a typical selection as you have redefined svg to be a transition rather than a generic selection:
var svg = d3.select("#chart").transition();
Any selection using this svg variable will return a transition (from the API documentation), for example with transition.selectAll():
For each selected element, selects all descendant elements that match
the specified selector string, if any, and returns a transition on the
resulting selection.
For transitions, the .data method is not available.
If you use d3.selectAll('circle') you will have more success. Alternatively, you could drop the .transition() when you define svg and apply it only to individual elements:
var svg = d3.select('#chart');
svg.select(".line").transition()
.duration(1000).attr("d", valueline(data));
...
Here is an updated fiddle taking the latter approach.
Also, for your update transition you might want to change scale and values you are using to get your new x,y values (to match your variable names):
//Update all circles
svg.selectAll("circle")
.data(data)
.transition()
.duration(1000)
.attr("cx", function(d) {
return x(d.date);
})
.attr("cy", function(d) {
return y(d.close);
});
}

d3.js: Unusual Error

I have this d3.js project donut chart. For some reason, I am not able to access the data with in the onmousemove. The i value become zero is all the functions I pass within that event. I want to access the data of the particular slice where the mouse has moved.
How do I resolve this? Someone pls hlp!
Here is my code so far:
piesvg.selectAll("path")
.data(pie(data))
.enter().append("g")
.attr('class', 'slice')
var slice = d3.selectAll('g.slice')
.append('path')
.each(function(d) {
d.outerRadius = outerRadius - 20;
})
.attr("d", arc)
.attr('fill', function(d, i) {
return colorspie(i)
})
.on("mouseover", arcTween(outerRadius, 0))
.on("mouseout", arcTween(outerRadius - 20, 150))
.on("mousemove", function(data){
piesvg.select(".text-tooltip")
.attr("fill", function(d,i){return colorspie(i)})
.text(function(d, i){return d[i].domain + ":" + parseInt(d[i].value * 20)}); //Considers i as 0, so no matter whichever slice the mouse is on, the data of only first one is shown
});
Here is the full code:
https://jsfiddle.net/QuikProBro/xveyLfyd/1/
I dont know how to add external files in js fiddle so it doesn't work....
Here is the .tsv that is missing:
value domain
1.3038675 Cloud
2.2541437 Networking
0.15469614 Security
0.8287293 Storage
0.7292818 Analytics
0.61878455 Intelligence
1.7016574 Infra
0.4088398 Platform
Your piesvg.select is bound to be zero-indexed for i and in all probability undefined for d as it takes those values from a single tooltip element, not the slices. Hard to be 100% sure from the snippet, but I suspect you're wanting to access and use the 'data' and 'i' from the original selectAll on the slices.
.on("mousemove", function(d, i){
piesvg.select(".text-tooltip")
.attr("fill", colorspie(i))
.text(d.data.domain + ":" + parseInt(d.data.value * 20));
});
Edited as pie slices store original data in d.data property ^^^

Assigning __data__ from parent node to child with javascript & d3js

I am creating a legend (var legend) for my d3js chart. The data is bound to the parent 'g' element, specifically a string (label) that I need to get at in the child 'text' element.
QUESTION: How to assign the parent data from 'g' element to the child text element?
Code:
var legend = svg.selectAll(".legend")
.data(color.domain().slice().reverse()) // color is array of strings with length = 6
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(-20," + i * 20 + ")"; });
legend.data(headers).enter().append("text") // headers is array of strings with length = 6
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return this.parentElement.__data__; }); // not working code here
Thanks! Full code: https://github.com/DeBraid/www.cacheflow.ca/blob/master/styles/js/d3kickchart.js
You're working much harder than you need to doing all this manual bookkeeping. Tying together different representations of the same data is one of D3's strong suits. Generally you want all your records in a single array as denormalized as possible, rather than trying to manually use indexes and multiple lists to correlate information.
On line 21 where you create the data records, simply add an additional property to hold the range label, and use these ranges as the domain of the color axis. (You might also look into d3's nest operator...)
Your legend can then be something as simple as this (jsfiddle):
var categories = ["foo", "bar", "baz", "qux", "zoo", "yaw"];
var color = d3.scale.ordinal()
.domain(categories)
.range(["#98ABC5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c"]);
d3.select("#legend")
.selectAll("div")
.data(categories)
.enter()
.append("div")
.style("background-color", color)
.text(function(d) { return d });
EDIT: The marked correct answer was the way to go. Below is the poor solution I hacked together.
Ended up with a pretty whacky bit of code here, but it works! HT Lars for suggesting adding .select() which was missing before.
legend.selectAll("text .legend")
.data([headers.slice().reverse()]) // data takes an array (headers), wrapped in [], order reversed
.enter().append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d,i,j) { return d[j]; }); // ** key **
Where 'j' is the index of the group containing the current element. Hence, using this.parentNode.data or the like is not needed, since we can access parent 'g' with the above. Not 100% sure on the mechanics of this, but it works! Feel free to elaborate on this methodology if you have cleaner way.

Basic D3.js: how to use joins within a function?

I'm getting to grips with D3.js. I would like to write a function that draws one set of dots with one set of data, then another set of dots with another set of data.
I have written this, but the second set of dots is over-writing the first set of dots! How can I rewrite it without the selectAll so that I correctly end up with two sets of dots?
function drawDots(mydata) {
focus.selectAll(".dot").data(mydata)
.enter().append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 3.5);
}
drawDots(data[0]);
drawDots(data[1]);
(NB: This is a simplification. Basically I want to know how to use .enter() with a function call.)
you need to give two sets of data distinct class names. Right now both get tagged with the same class (".dot"), but if they represent different sets, you also need to be able to distinguish between them. E.g.:
function drawDots(mydata, name) {
focus.selectAll(".dot"+"."+name).data(mydata)
.enter().append("circle")
.attr("class", "dot" + " " + name)
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 3.5);
}
drawDots(data[0], "set1");
drawDots(data[1], "set2");
I've only used d3js for building force graphs, but I think in your case you need to add the nodes first to the visualization, then invoke enter() and then fetch what is in the graph.
function drawDots(mydata)
{
myD3Object.nodes(myData).start();
focus.selectAll(".dot").data(myD3Object.nodes())
.enter().append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 3.5);
}

Categories

Resources