d3.js Respect weights for each treemap node - javascript

I'm currently working on a project using a modified Zoomable Treemap (http://bost.ocks.org/mike/treemap/) in d3.js but I have run into some difficulty in implementing this specific behavior that I'm looking for.
Ideally, each node in the tree would be sized according to its specified weight rather than the sum of the weights of its children. The only relevant question I could find (d3.js - Treemap where parent's value is greater than sum of its children) isn't quite what I'm looking for, though it could potentially be used to create a similar effect. However, attempting to use the filtered dummy node method to keep the parent's size constant when its children are modified results in ugly transitioning artifacts. I assume this due to the higher level being aware of the dummy node, but the lower level not being aware (as it is filtered, so it is not visible).
How might I go about implementing this behavior?

As it turns out, the implementation that I was using already had a function (accumulate(d)) that was responsible for this behavior. I solved this issue by rewriting it to always return the node's value directly like so:
function accumulate(d) {
var result = (d._children = d.children)
? d.value
: d.value;
if(d.children){
d.children.forEach(accumulate);
}
return result;
}
Now the nodes scale independently of their children as desired.
Hopefully this saves someone else some confusion in the future.

Related

Make two instances of d3.forceCollide() play nice together

I want two instances of d3.forceCollide(). In one, every node is pushed away from one another to prevent overlap. In the second, only a subset of nodes are pushed away from one another, with a much bigger radius.
To accomplish the second force, I tweak the initialize method to filter the incoming nodes, like so:
function selective(force,filter){
var init = force.initialize;
force.initialize = function(_){return init(_.filter(filter));};
return force;
}
var dpi = 90; // approximate pixels per inch in SVG
var size = dpi * (1/4); // quarter-inch unit size
var universally_applied =
d3.forceCollide()
.radius(size)
.strength(1);
var selectively_applied =
selective(
d3.forceCollide(),
function(d){return d.id === color;}
)
.radius(size*5)
.strength(1);
}
Now, this ALMOST works. I created a fiddle to see it in action: https://jsfiddle.net/jarrowwx/0dax43ue/38/ - every colored circle is supposed to repel every other circle of the same color, from a distance. Every other color, it just bumps into and pushes it out of the way.
If I do not change the order in which things are defined, then the selectively applied force is ONLY applied to the first color (red). If I shuffle the data array before applying forces, it is difficult to define exactly what happens, but the force is applied to some circles and not most of the others, even among the same color.
Any ideas what is going on here, or how to fix it?
The D3 team decided that this behavior was a bug (Isolating forces to a subset of nodes? #72), and fixed it. The fix was included in version 1.0.4 of d3-force, which is available as part of the full D3 build as of version 4.4.0.
The problem is resolved using the solution suggested by "Partial forces on nodes in D3.js", and the code works as intended now.

Is it possible to save extra data in the node object of a forced layout

I'm building a forced layout graph using the d3 library, and I'm using it to visualize graph data.
I would like to show a human-readable label over the node, and then when clicked, query for graph-connected properties of that node (and the node itself has a resource uri that I would like to have stored in the data per node).
is it possible to store that extra key/val in the node object? if not, how would you go about solving that problem?
Totally possible, and often desirable.
The force layout is initialized with a series of nodes (array of objects). Some params are expected to be present in those nodes (like weight or fixed) and other params are applied to those nodes by the force layout (like x, y, px, py). Any other params that you hang on those objects won't break anything and, more importantly, the force layout will not strip them out; they're persisted and are available to you when you render the layout, which is convenient and useful.
In fact, the array of objects you pass to the force layout can be an array of instances of whatever you're using to fetch and store the data (e.g. Backbone models). If doing so, there is a risk that you'd run into name collision. Like, if your model has a method called x(), and you pass that model into the force layout, then the layout would overwrite it with the computed x position. So it may be wiser to instead link to the associated model via a property of the node, but it's not inherently required.
UPDATE
For an example of how nodes can have extra key/val pairs that are unrelated to what the force layout works with, see mbostock's Les Misérables visualization. Specifically, see how d.group, which is part of the loaded json, is used to determine the node color when rendered, but doesn't have any bearing on the behavior of the nodes. That would be equivalent to what you're asking to do for showing a human readable label.
Now, if you want to integrate with instances of some class, eg Backbone.Models, you could do something like this (drawing on the Les Misérables example linked to above):
d3.json("miserables.json", function(error, graph) {
var models = graph.nodes.map(function(nodeJson) {
return new Backbone.Model(nodeJson)
});
force
.nodes(models)
.links(graph.links)
.start();
});
Since now these are Backbone models, you have to use get() for properties. So, when rendering the layout, instead of d.group, it would be
// d is a Backbone model now
.style("fill", function(d) { return color(d.get('group')); })
As I cautioned previously, you have to keep in mind that the force layout knows nothing about these being instances of Backbone models or whatnot. So internally it's not going to call something like node.set('x', ...) when it's computing node positions. It will, as before run node.x = ..., so you'll now have Backbone models that also have x, y properties applied to it by the force layout. If for some reason your model extends a Backbone model, and adds a method to it called x: function() { ... }, that method will get overridden (with the x position value) by the layout. So you have to be mindful of it, and if you need to work around it, then don't use Backbone models for nodes. Instead, use generic objects, as mbostock's example does, and store associated instances of your models on the nodes. Like
d3.json("miserables.json", function(error, graph) {
graph.nodes.forEach(function(nodeJson) {
bbModel = new Backbone.Model(nodeJson);
nodeJson.model = bbModel
});
force
.nodes(graph.nodes)
.links(graph.links)
.start();
});
Now rendering looks like this:
.style("fill", function(d) { return color(d.bbModel.get('group')); })

problems with latest cluster force layout example

Based on this work: http://bl.ocks.org/mbostock/7882658
If I substitute the automatic nodes creation by a JSON.stringify() output of the automatically generated data like this...
var nodes = [
{"cluster":2,"radius":1.6180680659922448},
{"cluster":0,"radius":3.3575295077569},
{"cluster":1,"radius":0.9569281165554346},
{"cluster":3,"radius":10.7245554165012}
];
...I get an exception "cannot read property x of undefined" on the line:
var x = d.x - cluster.x,
This is inside the cluster(alpha) function. So, apparently the d3.map function that automatically generates the data is putting something in the structure that the JSON stringification has not caught? Maybe I am just overlooking something simple...help is appreciated. Thanks! Here is a fiddle to help out: http://jsfiddle.net/Nivaldo/FJ3qq/1/
I commented out the code that is not working. Also, another detail, it does not seem like the original code as i left it (except that i reduced the count of clusters and nodes) is actually handling the right number of distinct clusters. It should paint 4 different ones but is only painting with 3 colors.
The problem is that nodes is not the only data structure that needs to be initialised -- clusters needs to be as well. In particular, specific nodes are assigned to specific cluster indices. If you don't do that, things will break.
To fix, do something like
nodes.forEach(function(d) { clusters[d.cluster] = d; });
Complete jsfiddle here.

Sigma JS node animation

I want to move nodes in Sigma JS on click event. Say from position x 0.7 to position x -0.7.
Is it possible to move nodes in Sigma js, and if it is, kindly guide me to achieve that.
Yes, it is possible. I created a jsfiddle showing how to change node position, size, and color on mouse click here:
http://jsfiddle.net/kaliatech/P255K/
You can bind to custom "downnodes" events like this:
sigInst.bind('downnodes',onNodeDown);
The event callback contains an array of selected node ids. I don't know when it would be more than one when clicking. Perhaps when zoomed out in a complex graph.
One of the more subtle issues when using sigmajs, is that most methods, such as getNodes, return clones, not the instances themselves. This is to protect "private" data in the graph I think, especially data that can not be redrawn after initialization. To actually modify properties, you need to use the iterator methods. Even then, you are only given clones. The library updates the actual node instances using a list of predefined allowable properties. (See the checkNode function in graph.js).
After properties have been set, you then need to refresh/redraw the graph. While the "refresh" method would seem to be an obvious candidate, it did not work. I was able to get it to redraw using the draw method though. You will need to review the source code to understand the different parameters. Example:
function onNodeDown(evt) {
var sigmajs = evt.target;
var nodeId = evt.content[0];
sigmajs.iterNodes(function(n){
n.size = 10;
n.color = "#0000FF";
},[nodeId]);
sigmajs.draw(2, 2, 2, true);
};
For more advanced needs, the sigmajs website includes plugin examples showing other ways of getting mouse events and updating nodes dynamically. One is the advanced plugin example for a fisheye effect:
http://sigmajs.org/examples/a_plugin_example_advanced.html
Another is the example for accessing node attributes:
http://sigmajs.org/examples/more_node_info.html
The sigmajs documentation is weak, so you will need to review the source code to understand things.
There are plugins permitting to move isolated nodes from the graph.
Check
https://github.com/Linkurious/linkurious.js/blob/develop/examples/lasso.html

Charting thousands of points with dojo

I need to plot thousands of points, perhaps close to 50,000 with the dojo charting library. It works, but it's definitely very slow and lags the browser. Is there any way I can get better performance?
EDIT:
I solved by applying a render filter to the data. Essentially, I have a new item parameter called "render" which is set to false by my json source if the point is expected to overlap others. My DataSeries then queries for all points where render:true. This way all of the data is there still for non-visual sources that want all of the points, while my charts now run smoothly.
Psuedocode:
def is_overlapped(x, y, x_round, y_round)
rounded_x = round(x, x_round)
rounded_y = round(y, y_round)
hash = hash_xy(rounded_x, rounded_y)
if(#overlap_filter[hash].nil?)
#overlap_filter[hash] = true
return false
end
return true
end
x_round and y_round can be determined by the x and y ranges, say for example range / 100
I know this isn't probably exactly the answer you're looking for, but have you considered simply reducing the number of points you are plotting? I don't know the specific function of the graph(s), but I'd imagine most graphs with that many points are unnecessary; and no observer is going to be able to take that level of detail in.
Your solution could lie with graphing techniques rather than JavaScript. E.g. you could most likely vastly reduce the number of points and use a line graph instead of a scatter plot while still communicating similar levels of information to your intended target.

Categories