In D3 v4 with the force module, how do you update the parameters of the simulation once you have initialized the graph?
More precisely, I am trying to change the .forceLink and .forceManyBody of the force directed graph when the user clicks one of its nodes.
var node = svg
.append("g")
.attr("class", "gnodes")
.selectAll(".node")
.data(graph.nodes)
.enter()
.append("g")
.attr("class", "node")
.on('dblclick', connectedNodes); //calls for change in simulation parameters
So far I have been able to update it by duplicating the simulation under the connectedNodes function:
function connectedNodes() {
//new parameters
linkDistance = 5;
fCharge = -10;
//replicates the initial simulation code
simulation = d3.forceSimulation()
.force("link", d3.forceLink()
.id(function(d) {return d.id;})
.distance(linkDistance)
)
.force("collide", d3.forceCollide()
.radius(function(d){return d.r + 10})
.strength(1)
)
.force("charge", d3.forceManyBody()
.strength(fCharge)
)
.force("center", d3.forceCenter(width / 2, height / 2));
simulation.nodes(graph.nodes).on("tick", ticked);
simulation.force("link").links(graph.links);
Although this works it is very redundant. Is there a way in which the simulation can be refreshed with the new parameters? I have tried the following but it does not work
function connectedNodes() {
//new parameters
linkDistance = 5;
fCharge = -10;
//restart simulation with new parameters
simulation.restart();
}
You need a reference to the forces you would like to update. This can be done using either of two ways:
As pointed out by Cool Blue in their comment, you can easily get a reference to the force by calling simulation.force() passing in just the name of the force it was registered with in the first place. If we had, supposedly, created our simulation while passing in an anonymous, in-place force like so:
var simulation = d3.forceSimulation()
.force("link", d3.forceLink() // "link" is the name to register the force
.id(function(d) { return d.id; })
.distance(linkDistance)
);
Later on the force can be obtained by its name whenever needed:
var forceLink = simulation.force("link"); // Get the force by its name
Personally, I like this approach and would prefer it over the second one, whenever possible, because I do not like having to many references / variables around.
Keep a reference to the force when creating it.
var forceLink = d3.forceLink() // Keep a reference to the force
.id(function(d) { return d.id; })
.distance(linkDistance);
var simulation = d3.forceSimulation()
.force("link", forceLink ) // Pass the reference when creating the simulation
No matter, which way you chose, you may then update your force by doing something like
linkDistance += 10;
forceLink.distance(linkDistance);
This will take the new value into account once the next tick is calculated. If the simulation has already come to a stop or you just want to heat it up again you may call
simulation.alpha(1).restart();
I have set up an example which shows these live updates when you hover over the SVG. This will update the linkDistance and restart the force layout.
Related
I have finally decided to saddle up and adopt d3 v5 syntax after years of using v3. After looking at some tutorials and examples, v5 syntax really struck me as sublime. The readability is far improved and it seems easier to integrate multiple data sources.
To my dismay, and despite my reverence of it, I couldn't quite build a visual from scratch with the new Promise syntax. Here is my simple graph: (note I'm using hard coded data for the sake of this post, and I have commented out the .csv() call that I'd actually use. It should still be functionally the same)
var margins = {top:50, right:50, bottom:50, left:50};
var width = window.innerWidth - margins.left - margins.right;
var height = window.innerHeight - margins.top - margins.bottom;
var sampleData = [
{'y':32, 'x':1},
{'y':20, 'x':2},
{'y':19, 'x':3},
{'y':12, 'x':4},
{'y':15, 'x':5},
{'y':19, 'x':6},
{'y':22, 'x':7},
{'y':26, 'x':8},
{'y':31, 'x':9},
{'y':36, 'x':10}
];
//var dataset = d3.csv("my-data.csv").then(function(data)
// {return data;
// });
var dataset = sampleData.then(function(data)
{return data;
});
var svg = d3.select('body').append('svg').attr('id','svg').attr('height','100%').attr('width','100%');
var myLine = dataset.then(function(data) {
Promise.all(data.map(function(d) {return {X:+d.x, Y:+d.y}}))//ensure numeric parsing
var xScale = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.X; }))
.range([0,width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(data, function(d) {return d.Y; }))
.range([height,0]);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
var line = d3.line()
.x(function(d) {return xScale(d.x); })
.y(function(d) {return yScale(d.y); });
var svg = d3.select('body').append('svg').attr('id','svg').attr('height','100%').attr('width','100%');
var graphGroup = svg.append('g')
.attr('transform',"translate("+margins.left+","+margins.top+")");
graphGroup.append('path')
.attr('d', function(d) {return line(data); });
graphGroup.append('g')
.attr('class', 'axis x')
.attr('transform', "translate(0,"+height+")")
.call(xAxis);
graphgroup.append('g')
.attr('class', 'axis y')
.call(yAxis);
});
I get this error in the console:
Uncaught TypeError: sampleData.then is not a function
Question
I take the point that Promise.all() and .then() are not always favorable for really simple data visuals, but I'd still like to know why I can't make the above script output a minimal line graph. From then, hopefully, I can slowly take the training wheels off and find my stride with v5.
I'm particularly confused with how to cast to numbers using the unary + with Promise.
Although there are many twists and turns when it comes to using Promises, it turns out that the actual changes required to port code to make use of the d3-fetch module in favor of the deprecated d3-request module are strikingly minimal. Loosely speaking, to adapt your—or any pre-v5—code to use the new d3-fetch module you just move the callback from one method to another. Thus, the former
d3.dsv(url, callback);
now becomes
d3.dsv(url).then(callback);
The only thing to be aware of is to check if the callback's signature matches the one expected for .then. This only becomes relevant, though, if your callback used two parameters to handle errors:
function callback(error, data) {
// Handle error
if (error) throw error;
// Manipulate data
}
With Promises this is split into two separated methods:
function onFullfilled(data) {
// Manipulate data
}
function onRejected(error) {
// Handle error
}
These callback can be used in two ways:
// 1.
d3.dsv(url).then(onFullfilled, onRejected);
// 2.
d3.dsv(url).then(onFullfilled).catch(onRejected);
Another important point is that you cannot return data from your callback (beware of the infamous "How do I return the response from an asynchronous call?"!). d3.dsv now returns a Promise, not your data; you have to handle the data inside your callback. If you become more skilled using Promises you might have a look into the await operator, though, which allows you to wait for a Promise and its fulfilled value. Although this is ECMAScript 2017 (ES8) syntax it has already seen wide-spread browser support.
That being the general case, now for your code: sampleData is an Array object which, of course, does not have a .then() method and, hence, the error. To make the code work there is not much to do apart from uncommenting the lines featuring d3.dsv and putting the relevant code handling data inside the callback.
If you really want to do an offline simulation with hardcoded data you can use Promise.resolve() which will return a Promise already resolved with the given value. In your case instead of
d3.csv("my-data.csv")
.then(function(data) { });
you can use
Promise.resolve(sampleDate)
.then(function(data) { }); // Same handler as above
I'm trying to create 50 SVG groups with each group containing a path that draws a particular US state. However, I can't figure out how to access the state data from the SVG group when creating the state path for that group. Can someone put me on the right track? Thanks!
var coords = [
{ 'id':'HI', 'class':'state hi', 'd': 'm 233.08751,519.30948 1.93993, ...' },
{ 'id':'AK', 'class':'state ak', 'd': 'm 158.07671,453.67502 -0.32332, ...'}
...
];
// Select the SVG
var svgContainer = d3.select('svg');
// Create groups with state data
var groups = svgContainer.selectAll('g')
.data(coords)
.enter()
.append('g');
// Create state path for each group
groups.each(function(g){
g.select('path')
.data(___NEED TO RETURN THE PATH DATA HERE___)
.enter()
.append('path');
});
Uncaught TypeError: g.select is not a function
Assuming your data is valid and you only want to handle new elements (no updates), this should work:
// Select the SVG
var svgContainer = d3.select('svg');
// Create groups with state data
var groups = svgContainer.selectAll('g')
.data(coords)
.enter()
.append('g')
.append('path')
.attr('d', function (d) { return d.d; });
I have created a pretty simplified fiddle with an example.
If you still want to use the .each function, you can proceed as follows:
groups.each(function (d, i){
// `this` (in this context) is bound to the current DOM element in the enter selection
d3.select(this).append('path').attr('d', d.d);
});
For further details, you can check the API documentation.
Hope it helps.
Introducing
I'm using Sequences Sunburst of d3.js for visualization of data.
I want to add a transition between two datasets (triggered by a button). I would like each arc to animate to display the new data.
Something like this: (1)Sunburst_Exemple, but without changing the accessor.
Research
In the (1)Sunburst_Example, the value accessor is modified. But I want to change the data, not the function that defines how to reach the data.
So, I searched a way for redefining data into path.
I was inspired by (2)Sunburst_Exemple, using each() method to store old values and attrTween() for transitions. But nothing is changing and I have the following error message:
Maximum call stack size exceeded . Maybe caused by the fact I have a method and I'm not in a global scope.
(2) link : _http://ninjapixel.io/StackOverflow/doughnutTransition.html
Then I have tried (3)Zoomable_Sunburst example, but nothing it happens in my case... .
(3) link : _http://bl.ocks.org/mbostock/4348373
My Example
Here is my example : JSFIDDLE
Problem is :
colors are lost
transition is not happening
I think I don't understand how transitioning is really working, I could have missed something that could help me in this case.
Any help ?
-
Listener of button call click method that redefined nodes and path.
/*CHANGING DATA*/
function click(d){
d3.select("#container").selectAll("path").remove();
var nodes = partition.nodes(d)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
}) ;
var path = vis.data([d]).selectAll("path")
.data(nodes)
.enter().append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return colors[d.name]; })
.style("opacity", 1)
.on("mouseover", mouseover)
.each(stash)
.transition()
.duration(750)
.attrTween("d", arcTween);
;
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
}
_
// Stash the old values for transition.
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
// Interpolate the arcs in data space.
function arcTween(a) {
var i = d3.interpolate({x: a.x0, dx: a.dx0}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
}
Data Characteristics :
the root node is the same for the two datasets.
the structure is globally the same, only the values are changing.
Some fields disappear but we can add them with value equal to 0.
I am trying to get some dart code working with D3, but I am having really strange problems when I call d3.layout.force().start(). In my code below, I have commented out everything below force.size(). When I run this code and look at the console output in browser, I get the result of the single js.context.console.log() And it prints out my object just as written when assigning word_map. The problem is that as soon as I uncomment force.start() (only that one line btw) the object word_map starts to change and the console.log prints out an object whos x and y values are NAN.
Why does force.start() change the x and y values of my object to NAN and how is this change being shown in my console.log statement when the console.log occurs long before the force.start() is being called. It is like force.start() is being called first and the console.log is being called second.
import 'dart:html';
import 'package:js/js.dart' as js;
import 'dart:async';
import 'dart:js';
List<Map<String, String >> word_map;
void main() {
draw_visualization();
}
void draw_visualization(){
word_map =new List<Map<String,String>>();
word_map= [{'word':'hello','x':30.0,'y':20.0, 'fixed':false}];
JsObject word_list_js = new JsObject.jsify(word_map);
js.context.console.log(word_list_js);
print ("Made it here first");
var force = d3.layout.force()
.nodes(word_list_js)
.links(js.array([]))
.size([600,400]);
/* .start();
var chart = d3.select("body")
.append("svg")
.attr('width', 600)
.attr('height', 400);
var node = chart.selectAll('.node')
.data(word_list_js).enter().append('circle')
.attr('class','node')
.attr('r', 5)
.attr('cx', new js.FunctionProxy((d,i,e){return d.x;}))
.attr('cy',new js.FunctionProxy((d,i,e){return d.y;}))
.style('fill', 'blue')
force.on('tick', new js.FunctionProxy((e){
node.attr("cx", new js.FunctionProxy((d,i,e){
if (d.x > 0 && d.x<4000){print(d.x);}
return d.x;
}))
.attr("cy", new js.FunctionProxy((d,i,e){
return d.y;
})
);
}));
*/
}
I have to admit, I am a bit confused on how the whole proxy system works and Dart's async is giving me a bit of trouble too. I just don't see how the force.start function would even effect the previous console.log statement.
Overall, I just want my object to stop getting f'd up by the force layout. NAN's for x and why make it so my circles do not draw correctly.
EDIT: It seems to have to do with gravity. After adding .gravity(0) before .start() it does not compute x and y to nan. I did this in a hunch that one of the 2 forces of force layout was causing the issue. I still don't understand why gravity(0) fixes this.
You shouldn't mix package:js and dart:js.
remove import 'dart:js';.
replace new JsObject.jsify(word_map) with js.map(word_map)
I've recently passed from d3.v2 to d3.v3, and am trying to understand the differences in the transition mechanisms.
In the code underneath, I'm trying to make a bar graph that, when drawn, has bars that increase in height via a transition. This code works without issue in d3.v2, but in v3, the transition seems to happen "instantly" (the height is immediately set to the end value).
graph.enter()//for each bucket
.append('g')
.attr('transform',function(d,i){ return 'translate('+(xBand(i))+')';})
.attr('width',xBand.rangeBand())
.each(function(data,index){//here we are working on the selection for a single bucket
var $this=d3.select(this); //this refers to the group selection
var currentY=0;
var rects=$this.selectAll('rect')
.data(data.values);
rects.enter()
.insert('rect')
.attr('group-id',me.groupId)
.attr('y',Hats.accessor('y'))
.attr('width',xBand.rangeBand())
.attr('fill',(function(elt){ return me.colors(me.groupId(elt));}));
rects.transition()
.duration(750)
.attr('height',(function(elt){
var h=_.compose(heightScale,me.values)(elt);
d3.select(this).attr('y',currentY);
currentY+=h;
return h;
}));
});
Try setting a starting height in your enter selection:
rects.enter()
.insert('rect')
.attr('group-id',me.groupId)
.attr('y',Hats.accessor('y'))
.attr('width',xBand.rangeBand())
.attr('fill',(function(elt){ return me.colors(me.groupId(elt));}))
.attr('height', 0);
rects.transition()
.duration(750)
.attr('height',(function(elt){
var h=_.compose(heightScale,me.values)(elt);
d3.select(this).attr('y',currentY);
currentY+=h;
return h;
}));