Derived from the D3.js example force_cluster.html and gist 3071239.
See the graph in various states of undress, click to see the full-rez centerfold:
<img src="https://raw.github.com/gist/3104394/Screenshot.jpg" alt=rel="the full bluntal nugity of it (4000 x 3000px)" />
- collapse nodes into groups (group nodes); graph starts in fully collapsed mode
- 3 display modes per group:
- nodes+links collapsed,
- nodes collapsed (but each outgoing link shown individually),
- group expanded (with hull wrapped around the nodes) Click on a node to go through the display modes; click on the 'hull' to collapse.
- code showcases a dual force layout, i.e. a layout where one force, with its own set of nodes, drives a second force with another set of nodes & links.
- Various ways to tune a force layout: custom functions for distance, charge, etc. and custom corrections and constraint processing in the force.tick() custom event handler.
- shows a way to stop the force layout faster than it normally does ('fast stop')
- includes fixes for
force.drag
behaviour which is off the wall when you wait long enough while dragging forlayout.force
to stop theforce.tick
sequence (timer).
- Click on node to expand or collapse. When a node has 'bundled' outgoing links, the first click will expand only those (a.k.a. 2nd display mode / expand state = 1), the next click will then expand the group node itself.
- Click on hull (which shows up when you expanded a group node) to collapse the group.
- Drag node to move entire graph around.
-
network()
is the one who takes care of (re)generating the nodes and links from the original data, based on theexpand[]
info, i.e. which group(s) should be shown in expanded form and which shouldn't. -
only group nodes are expected to have a
.size
attribute (read: your own JSON should use that attribute for any node). Same goes for the fields.group_data
and.link_count
: all of these are expected to be generated by thenetwork()
call. (.group_data
is a reference to the group (x/y/size/link_count) for a node,group_node.link_count
counts the number of links between groups.) -
you'll very probably have to tweak
.gravity
,.charge
,.linkDistance
and maybe also.linkStrength
to make your own graphs look good. Compare the final layout of this graph with the ones produced by the v2.9.6 force_cluster.html D3 code: note the generally quite different position of the groups which have only a single link to other groups; that and other differences are all due to the 4 aforementionedforce
parameters. -
You may want to download the git repo and scan the commits; I will not say this is exemplary usage in terms of git commits, but that way you can better inspect the development that went into this tuned layout; observe mistakes, etc.
-
I got the wicked idea of a chained force layout while climbing out of a fever; I did first try to get this vision going using a single force layout, but the tuning became unmanagable before it became a success there. The chained force layout takes the nodes (groups and individual nodes for expanded groups) and their links in force1, anneals them and takes the resulting layout as a '.fixed=1' node set for the second graph, which adds 2 helper nodes (dots) per actual link, so that we have second force (force2) to calculate the distribution/positioning of our curves which are to be the lines representing the original, individual links. Here we've chosen to use 2 dots, so is makes sense to use a cubic spline then, which served us well. With only one dot in between nodes, one might have done well too, but that single dot would have been influenced by both source and target .fixed nodes; now we only have one side to look at, if we really need some very particular tuning/rendering/force2 annealing. The whole idea of the second force was to have the ability to make
d3.layout.force
behave completely different for the curves than for the node+link set themselves with 'zero hassle' (tongue in cheek).
Heck, every node set will need its own tweaks and tugs if you want to have it look exactly right all the time.
After the initial botched attempts, I did it all over again from scratch and wrote down at high level what the focus of attention was and in what order. This might help you to 'professionalize' your own tunings, pulling them out of the 'trial & error' era.
Note that the global var
debug
can be zet to 0, 1 or 2; we start with debug=2 so that we can see the underlying first 'force' layout at work: those are our red links and our nodes.
-
start with default force layouts, so no custom distance, charge, or whatever. Strip & Simple, that's the way!
-
we concentrate on the group views first! (
expand[n] = {0,1}
)-
increase force.distance for all as the nodes are way too close together:
force.distance = 300
-
and they are a'huggin'.
force.charge = -5000
( Note the minus there: dial up the rejection quite a bit there! )
-
-
now we attend to the 'singles': group nodes with only 1 or just a few links look much better when they are repelled by the gravity, rather than attracted (and thus pulled into the fray): undo the gravity and even push out. (This last bit will be removed before we are done tuning: 'undo-ing' the gravity was good enough in the end; reverse engineered from the d3.js source code, so to say. The Source, Luke, The Source!)
-
now the singles are too far off, their links are horribly long: reduce their distance:
force.distance = 100/300
(100 for singles, 300 for the rest of 'em) -
when expanding and playing, nodes fly off the screen. That's undesirable. Constrain the nodes to the force area (width/height). This is done as a custom constraint in the force.tick() event handler:
force.on("tick", ...)
-
initially we had only treated 1-link group nodes as 'singles' (hence the name), but it turns out that 2-link nodes, and in a very minor way maybe 3-link nodes as well, would benefit from this 'push out' behaviour. Go ahead, include 2-link nodes. Trial showed that treating them exactly likes 'true singles' is a-okay, for now.
-
-
Now it's time to have a closer look at the expanded groups, as the basic group layout is good enough:
-
possibly adjust the ditance for the links which sit entirely within the same group, in order to keep those nodes close together and the groups far apart, even when they are expanded.
-
it turns out that 1-link 'real nodes' (i.e. nodes which came out of the JSON data and are NOT a group) need 'push out' tweaking. More conditionals and maybe a formula?
-
And also tweak
force.distance
andforce.charge
for these 'real nodes' with low link counts; now they often get far away from the node(s) they link to and that's ugly in its own right. -
Iterate a few times between adjusting the 'singles' tweaks so that both 'group node' singles and 'real node' singles look their best on screen.
-
-
Now we have arrived at a state where the basic node layout (I don't bother with the links! force2 + bezier curves will take care of that!) is nice. Hence we switch to the force2 force layout and start to analyze and tweak that one, while traying to keep these tweaks from influencing the initial force layout, which we have been tweaking up to now and just looking good.
-
start out with a 'vanilla' force2 layout. Gravity has no function here as all helper nodes are to be bound by the nodes from the initial force layout, whose clones are marked as .fixed=1 so those stay where they are in force2 whatever we do in here.
-
force2.gravity = 0.0
-
-
The curves for group links and 'real node' to group links are looking good already, so we concentrate on the within-group links which are shown in expanded mode (
expand[n] = 2
):-
reducing
force2.distance
doesn't seem to do anything for us, so instead we attackforce2.charge
first: reduce the charge of fixed nodes and for helper nodes for links which stay in the same group -
Now go back and revisit the
force2.distance
and reduce it for within-group link related nodes. I find that I have to reduce to 'ridiculously low levels'; that might be why the initial attack didn't deliver. -
iterate those force2 tweaks a few times until all link curves render nicely.
-
-
Done. (Unless, that is, of course, when you run into tougher nuts to crack. See the git commit history for a few things that didn't make it to the end; some of it might be useful for your layouts, but when you concoct a couple of tricks like I do and somewhere halfway through things start to look worse again instead of better, than be very willing to anihilate those tricks of yours and rewind, come up with a few other things to test and proceed again. Happy tweaking! )
Reworked the code to be testable. Still in D3 version 2.
View it here: Testable version