d3v4 Nesting force graphs - d3.js

I have a force directed graph in d3v4 and I'd like to situate another, smaller force graph around each node.
Here is an example of what I want to do, but this is in v3. I basically tried to take this pattern from there, and it didn't work. http://bl.ocks.org/djjupa/5655723
I thought to accomplish that by creating a new one inside node.each, but that doesn't appear to be working.
Here's my code to make the new node -- it appears to be the same as the code that is successfully instantiating the first forcegraph, but this is in a d3.each function on the d3 node group.
When I inspect the childnodes by console.logging them in the tick function, I see that it has a single element array _groups that has a 3 element array with 3 undefined elements in it. Hmmm - could that be the problem?
135 console.log('instantiateChildForceGraph', parent, ix)
136
137 let subFg = d3.select(this)
138
139 parent.tokens.fixed = true
140 parent.tokens.x = 0
141 parent.tokens.y = 0
142
143 let icon_size = 16
144
145 let childNodes = parent.tokens.children
146
147 let childSimulation = d3.forceSimulation()
148 .force('collide', d3.forceCollide( (d) => { return 150 }).iterations(16))
149 .force('center', d3.forceCenter(window.innerWidth/2, window.innerHeight/2))
150 .force('link', d3.forceLink()
151 .id((d) => { return d.index + 1 })
152 .distance(200)
153 .strength(1))
154 .force('charge', d3.forceManyBody())
155 .force('x', d3.forceX())
156 .force('y', d3.forceY())
157 .alphaTarget(1)
158
159 let childNode = subFg.selectAll('.token')
160 .data(childNodes, (d) => { return d.id })
161
162 let childNodeEnter = childNode
163 .enter()
164 .append('g')
165 .attr('class', 'token-node-' + parent.id )
166 .attr('transform', (d) => { return 'translate(' + d.x + ',' + d.y + ')' })
167
168 childNodeEnter
169 .append('circle')
170 .attr('class', (d) => { return 'token token-' + d.source })
171 .attr('r', 5)
172 .style('fill', 'black')
173 .style('stroke', 'black')
174
175 childNode.exit().remove()
176
177 // let childNode = subFg.select('g.token-node-' + parent.id)
178 // .selectAll('.token')
179 // .data(childNodes, (d) => { return d.id })
180 // .enter()
181 // .attr('transform', (d) => { console.log('d', d); return 'translate(' + d.x ? d.x : 0 + ',' + d.y ? d.y : 0 + ')' })
182 // .exit()
183 // .remove()
184
185 console.log('childSimulation', childSimulation)
186 console.log('childNodes', childNodes)
187
188 console.log('no')
189 childSimulation.nodes(childNodes)
190 childSimulation.force('link').links()
191 childSimulation.on('tick', function(d) {
192 console.log('childnode', childNode)
193 childNode.attr('transform', (d) => { return 'translate(' + d.x + ',' + d.y + ')' })
194 })
195 }
196

So upon inspecting childNode I saw this particular code was actually returning 3 elements of undefined. So I had to refine the selection process for childNode to select the root <g> element of these sub nodes.
But that wasn't the only problem - I did that quickly after making these posts as a matter of fact.
But there was another problem that was much more elusive. Everything was working out, but I couldn't see the sub force animation in the browser.
It turns out that is because, probably somehow since it was a nested force graph, it was animating it with an offset of about 700 pixels, so I simply couldn't see it. That was solved by simply changing the transform function to negate the distance of the offset.

Related

d3 v4 force graph doesn't stop moving after applying links

I have this code that is working great except that the only way I can figure out to stop making it spin is to turn up the velocityDecay, which makes the animation the point when a new node is introduced to be quite slow -- and doesn't actually stabilize the animation -- it's always moving slightly.
Here's the relevant parts of the code (or tell me if there are other important components):
```
60 let simulation = d3.forceSimulation()
61 .force('link', d3.forceLink()
62 .id((d) => { return d.index + 1 })
63 .distance(200)
64 .strength(1))
65 .force('charge', d3.forceManyBody())
66 .force('x', d3.forceX())
67 .force('y', d3.forceY())
68 .alphaTarget(1)
69 .on('tick', ticked)
70 .force('center', d3.forceCenter(window.innerWidth/2, window.innerHeight/2))
71 .force('collide', d3.forceCollide( (d) => { return 150 }).iterations(16))
72
73 svg.append('g').attr('class', 'links')
74
75 let link = svg.select('.links')
76 .selectAll('.link')
77 .data(links)
78 .enter().append('path')
79 .attr('class', 'link')
80 .attr('fill', 'transparent')
81 .attr('stroke', 'black')
82 .attr('stroke-width', '10px')
83 .exit()
84 .remove()
107 link = link.data(links)
108 .enter().append('path')
109 .attr('class', (d) => {
110 return 'link link-' + d.source + '-' + d.target
111 })
112 .attr('fill', 'transparent')
113 .attr('stroke', 'black')
114 .attr('stroke-width', '10px')
115 .merge(link)
116 link.exit().remove()
147 simulation.nodes(nodes)
148 .on('tick', ticked)
149 simulation
150 .force('charge', d3.forceManyBody())
151 // .force('center', d3.forceCenter(window.innerWidth/2, window.innerHeight/2))
152 // .force('collide', d3.forceCollide( (d) => { return 150 }).iterations(156))
153 // .alphaTarget(1)
154 .alphaDecay(.5)
155 // .alpha(1)
156 // .velocityDecay(.95)
157 .force('link', d3.forceLink()
158 .id((d) => { return d.index + 1})
159 .distance(200)
160 .iterations(20)
161 .strength(1))
162 .force('x', d3.forceX())
163 .force('y', d3.forceY())
164 .force('link').links(links)
165 simulation.restart()
```

Using d3.filter in an update function

I am drawing a bar chart based on data for two different regions "lombardy" and "emiglia".
Below is an outtake of my code. First I draw the chart, by filtering on the region "lombardy". Now I would like to update the bars via a transition to use "d.value" from "emiglia".
g.select(".gdp").selectAll(".gdp-rect")
.data(data)
.enter()
.append("rect")
.classed("gdp-rect", true)
.filter(function(d) {return (d.type == "gdp") })
.filter(function(d) {return (d.region == "lombardy") })
.attr('x', function(d, i) {
return i * (width / 4)
})
.attr('y', function(d) {
return h - yBarScale(d.value)
})
.attr('width', width / 4 - barPadding)
.attr('height', function(d) {
return yBarScale(d.value)
})
.attr("fill", "#CCC")
.attr("opacity", 1);
function emiglia() {
g.selectAll(".gdp-rect")
.transition()
.duration(600)
.filter(function(d) {return (d.region == "gdp") })
.filter(function(d) {return (d.region == "emiglia") })
.attr('y', function(d) {
return h - yBarScale(d.value)
})
.attr('height', function(d) {
return yBarScale(d.value)
})
}
Is it possible to update based on d3.filter? How can I toggle d.value for both regions?
data.tsv
type region year value
gdp lombardy 2004 70
gdp lombardy 2008 50
gdp lombardy 2012 30
gdp lombardy 2016 110
gdp emiglia 2004 10
gdp emiglia 2008 15
gdp emiglia 2012 23
gdp emiglia 2016 70
There are several ways to do that, this is one of them:
To use filter in your update function, you have first to load the data...
d3.csv("data.csv", function(data){
...in an array called data. Then, inside d3.csv, you create your update function (here I'll call it draw) having the region as an argument, and filtering data based on this argument:
function draw(myRegion){
var newData = data.filter(function(d){ return d.region == myRegion})
Now you use this new array (newData) to draw your bars.
This is an example using buttons to call the function draw: https://plnkr.co/edit/QCt1XgWxrSM8MlFijyxb?p=preview
(Caveat: in this example I'm using D3 v4.x, but I see that you're using D3 v3. So, this is just an example for you to see the general idea.)
Just a final tip: normally, we don't filter the data to change a visualization like this. The normal approach, let's call it the D3 way is simply creating your dataset with both Lombardy and Emiglia as columns:
type year lombardy emiglia
gdp 2004 70 10
gdp 2008 50 15
gdp 2012 30 23
gdp 2016 110 70
That way, we could simply set the width of the bars using:
.attr("width", function(d){ return xScale(d[region])});
And setting region according to the column (Lombardy or Emiglia).

Rotate D3 symbol

I'm rendering a d3 symbol that looks like this:
svg.append('path')
.attr("d", d3.svg.symbol().type("triangle-up").size(10))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", "red")
I'd like to rotate this triangle so that the triangle points left <|. How can I rotate this symbol while keeping it at the same position in my viz? I've been trying to do the following, but the symbol moves to the upper left corner of my viz (it doesn't stay in the position created by the transformation):
svg.append('path')
.attr("d", d3.svg.symbol().type("triangle-up").size(10))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.attr("transform", "rotate(-45)")
.style("fill", "red")
The problem is that the second call to set transform overrides the first value here:
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.attr("transform", "rotate(-45)") // Only this value will remain as the value of the attr
To fix it, you should append the rotation to the original value of transform:
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ") rotate(-45)"; })
Another solution is to put the symbol in nested g elements and apply individual transforms to each of them, example.

D3: How do I chain transitions for nested data?

I'm attempting to make a visualization with a multistage animation. Here's a contrived fiddle illustrating my problem (code below).
In this visualization the boxes in each row should turn green when the entire group has finished moving to the right column. IOW, when the first row (containing 3 boxes) is entirely in the right column, all the boxes should turn from black to green, but the second row, having only partially moved to the right column at this point, would remain black until it, too, is completely in the right column.
I'm having a hard time designing this transition.
Basic chaining without a delay immediately turns each box green once its finished moving (this is how it's working currently). Not good enough.
On the other hand creating a delay for the chain is difficult, since the effective delay per group is based on the number of boxes it has and I don't think this count is available to me.
It's like I need the transition to happen at mixed levels of granularity.
How should I go about doing this?
The fiddle (code below)
var data = [
["x", "y", "z"],
["a", "b", "c", "d", "e"]
];
var svg = d3.select("svg");
var group = svg.selectAll("g").data(data)
.enter()
.append("g")
.attr("transform", function(d, i) {
return "translate(0, " + (40 * i) + ")";
});
var box = group.selectAll("rect")
.data(function(d) { return d; });
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) { return 60 + 30 * i; })
.transition()
.delay(function(d, i) { return 250 + 500 * i; })
.attr("x", function(d, i) { return 300 + 30 * i; })
.transition()
.attr("style", "fill:green");
// I probably need a delay here but it'd be based off the
// number of elements in the nested data and I don't know
// how to get that count
.attr("style", "fill:green");
I manage to get the effect you want, it's a little tricky though. You can customize the behavior of a transition at the begining and end of a transition. If you add a function to the end of the transition that detects if the transitioned element is the last in the group, you select all the rectangles in the group and apply the change to them.
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) { return 60 + 30 * i; })
.transition()
.delay(function(d, i) { return 250 + 500 * i; })
.attr("x", function(d, i) { return 300 + 30 * i; })
.each('end', function(d, i) {
var g = d3.select(d3.select(this).node().parentNode),
n = g.selectAll('rect')[0].length;
if (i === n - 1) {
g.selectAll('rect').attr('fill', 'green');
}
});
More details in the transitions here, a working fiddle here.

Arc Based Text Alignment in D3

I'm trying to create a Radial Hub and Spoke diagram (http://bl.ocks.org/3169420) with D3 that uses arcs of a pie to represent relationships to and/or from a node of context. In other words, I'm trying to create a 360 degree view of a node's relationships.
In the example above, "Node 1" is in the center. I can't get the textual node names for related nodes #8 - #18 to align "outside" the wheel. Would anyone know what the best way is to do so?
The code that I'm using looks as follows...
// Add node names to the arcs, translated to the arc centroid and rotated.
arcs3.filter(function(d) {
return d.endAngle - d.startAngle > .2; }).append("svg:text")
.attr("dy", "5px")
.attr("angle", function(d) { return angle(d); })
.attr("text-anchor", function(d) { return angle(d) < 0 ? "start" : "end"; })
.attr("transform", function(d) { //set text'ss origin to the centroid
// Make sure to set these before calling arc.centroid
d.innerRadius = (height/2); // Set Inner Coordinate
d.outerRadius = (height/2); // Set Outer Coordinate
return "translate(" + arc3.centroid(d) + ")rotate(" + angle(d) + ")";
})
.style("fill", "Black")
.style("font", "normal 12px Arial")
.text(function(d) { return d.data.name; });
// Computes the angle of an arc, converting from radians to degrees.
function angle(d) {
var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
return a > 90 ? a - 180 : a;
}
The issue seems to be that I need to evaluate the X and Y coordinates of where the Node labels are placed. However, I can't seem to correctly find/access those X and Y values.
Thanks, in advance, for any help you can offer.
My Best,
Frank
Thanks to Ger Hobbelt for this... In short, the angle function needs to be parameterized:
function angle(d, offset, threshold) {
var a = (d.startAngle + d.endAngle) * 90 / Math.PI + offset;
return a > threshold ? a - 180 : a;
}
The complete answer can be found d3.js Google Group: https://groups.google.com/forum/?fromgroups#!topic/d3-js/08KVfNmlhRo
My Best,
Frank

Resources