lamchau
10/9/2014 - 2:57 PM

.gitignore

# Editor backup files
*.bak
*~
Code derived from the d3.js 'force' example:

The miserables.json file contains the weighted network of coappearances of
characters in Victor Hugo's novel /Les Miserables/. Nodes represent characters
as indicated by the labels, and edges connect any pair of characters that
appear in the same chapter of the book. The values on the edges are the number
of such coappearances. The data on coappearances were taken from D. E. Knuth,
"The Stanford GraphBase: A Platform for Combinatorial Computing",
Addison-Wesley, Reading, MA (1993).

The group labels were transcribed from "Finding and evaluating community
structure in networks" by M. E. J. Newman and M. Girvan.

Experiment ……

Derived from the D3.js example force_cluster.html and gist 3071239.

Pictures, my dear!

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)" />

Features

  • 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 for layout.force to stop the force.tick sequence (timer).

Usage

  • 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.

Notes

  • network() is the one who takes care of (re)generating the nodes and links from the original data, based on the expand[] 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 the network() 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 aforementioned force 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).

Tuning The Darn Thing

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 and force.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 attack force2.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! )

<!DOCTYPE html>
<html>
  <head>
    <title>Clustered Network</title>
    <script src="http://d3js.org/d3.v2.js"></script>

    <style type="text/css">
svg {
  border: 1px solid #ccc;
}
body {
  font: 10px sans-serif;
}
circle.node {
  fill: lightsteelblue;
  stroke: #555;
  stroke-width: 3px;
}
circle.leaf {
  stroke: #fff;
  stroke-width: 1.5px;
}
circle.link-expanded {
  stroke: #555;
  stroke-width: 3px;
  stroke-dasharray: 2px 4px;
}
circle.helper {
  stroke-width: 0;
  fill: #333;
  opacity: 0.6;
}
circle.center-of-mass {
  fill: white;
  stroke: red;
  stroke-width: 5px;
}
path.hull {
  fill: lightsteelblue;
  fill-opacity: 0.3;
}
path.hlink {
  stroke: #333;
  stroke-opacity: 0.5;
  fill: none;
  pointer-events: none;
}
line.link {
  stroke: red;
  stroke-opacity: 0.7;
  pointer-events: none;
}
    </style>
  </head>
  <body>
    <script type="text/javascript">
var width = 960,        // svg width
    height = 600,       // svg height
    dr = 4,             // default point radius
    off = 15,           // cluster hull offset
    expand = {},        // expanded clusters
    data, net, force, force2, hullg, hull, linkg, helper_linkg, link, hlink, nodeg, helper_nodeg, node, hnode,
    debug = 0; // 0: disable, 1: all, 2: only force2

var curve = d3.svg.line()
  .interpolate("cardinal-closed")
  .tension(.85);

var fill = d3.scale.category20();

function noop() { return false; }

function nodeid(n) {
  return n.size > 0 ? "_g_" + n.group + "_" + n.expansion : n.name;
}

function linkid(l) {
  var u = nodeid(l.source),
      v = nodeid(l.target);
  return u<v ? u+"|"+v : v+"|"+u;
}

function getGroup(n) { return n.group; }

function cycleState(d) {
  var g = d.group, s = expand[g] || 0;
  // it's no use 'expanding the intergroup links only' for nodes which only have 1 outside link for real:
  if (d.ig_link_count < 2)
    s = (s ? 0 : 2);
  else {
    s++; s %= 3;
  }
  return expand[g] = s;
}

// constructs the network to visualize
function network(data, prev) {
  expand = expand || {};
  var gm = {},    // group map
      nm = {},    // node map
      nml = {},   // node map for left-side 'link path helper nodes'
      nmr = {},   // node map for right-side 'link path helper nodes'
      nmimg = {}, // node map for cloned nodes for force2
      lm = {},    // link maps - lm ~ lml-lmm-lmr
      lml = {},
      lmm = {},
      lmr = {},
      gn = {},                  // previous group nodes
      gc = {},                  // previous group centroids
      nodes = [],               // output nodes
      links = [],               // output links
      helper_nodes = [],        // helper force graph nodes
      helper_links = [];        // helper force graph links
      helper_render_links = []; // helper force graph links
  var k;

  // process previous nodes for reuse or centroid calculation
  if (prev) {
    prev.nodes.forEach(function(n) {
      var i = getGroup(n), o;
      if (n.size > 0) {
        gn[i] = n;
        n.size = 0;
        n.ig_link_count = 0;
        n.link_count = 0;
        n.first_link = null;
        n.first_link_target = null;
      } else {
        o = gc[i] || (gc[i] = {x:0,y:0,count:0});
        o.x += n.x;
        o.y += n.y;
        o.count += 1; // we count regular nodes here, so .count is a measure for the number of nodes in the group
      }
    });
  }

  // determine nodes
  for (k=0; k<data.nodes.length; ++k) {
    var n = data.nodes[k],
        i = getGroup(n),
        expansion = expand[i] || 0,
        l = gm[i] || (gm[i]=gn[i]) || (gm[i]={group:i, size:0, nodes:[], ig_link_count:0, link_count:0, expansion: expansion}),
        img;

    // we need to create a NEW object when expansion changes from 0->1 for a group node
    // in order to break the references from the d3 selections, so that the next time
    // this group node will indeed land in the 'enter()' set
    if (l.expansion != expansion) {
      l = gn[i] = gm[i] = {group:l.group, x:l.x, y: l.y, size:l.size, nodes:l.nodes, ig_link_count:l.ig_link_count, link_count:l.link_count, expansion: expansion};
    }

    if (expansion == 2) {
      // the node should be directly visible
      nm[nodeid(n)] = n;
      img = {ref: n, x: n.x, y: n.y, size: n.size || 0, fixed: 1, id: nodeid(n)};
      nmimg[nodeid(n)] = img;
      nodes.push(n);
      helper_nodes.push(img);
      if (gn[i]) {
        // place new nodes at cluster location (plus jitter)
        n.x = gn[i].x + Math.random();
        n.y = gn[i].y + Math.random();
      }
    } else {
      // the node is part of a collapsed cluster
      if (l.size == 0) {
        // if new cluster, add to set and position at centroid of leaf nodes
        nm[nodeid(n)] = l;
        l.size = 1;                     // hack to make nodeid() work correctly for the new group node
        nm[nodeid(l)] = l;
        img = {ref: l, x: l.x, y: l.y, size: l.size || 0, fixed: 1, id: nodeid(l)};
        nmimg[nodeid(l)] = img;
        l.size = 0;                     // undo hack
        nmimg[nodeid(n)] = img;
        nodes.push(l);
        helper_nodes.push(img);
        if (gc[i]) {
          l.x = gc[i].x / gc[i].count;
          l.y = gc[i].y / gc[i].count;
        }
      } else {
        // have element node point to group node:
        nm[nodeid(n)] = l; // l = shortcut for: nm[nodeid(l)];
        nmimg[nodeid(n)] = nmimg[nodeid(l)];
      }
      l.nodes.push(n);
    }
    // always count group size as we also use it to tweak the force graph strengths/distances
    l.size += 1;
    n.group_data = l;
    n.link_count = 0;
    n.first_link = null;
    n.first_link_target = null;
  }

  // determine links
  for (k=0; k<data.links.length; ++k) {
    var e = data.links[k],
        u = getGroup(e.source),
        v = getGroup(e.target),
        rui, rvi, ui, vi, lu, rv, ustate, vstate, uimg, vimg,
        i, ix,
        l, ll, l_, lr;
    if (u != v) {
      gm[u].ig_link_count++;
      gm[v].ig_link_count++;
    }
    ustate = expand[u] || 0;
    vstate = expand[v] || 0;
    // while d3.layout.force does convert link.source and link.target NUMERIC values to direct node references,
    // it doesn't for other attributes, such as .real_source, so we do not use indexes in nm[] but direct node
    // references to skip the d3.layout.force implicit links conversion later on and ensure that both .source/.target
    // and .real_source/.real_target are of the same type and pointing at valid nodes.
    rui = nodeid(e.source);
    rvi = nodeid(e.target);
    u = nm[rui];
    v = nm[rvi];
    if (u == v) {
      // skip links from node to same (A-A); they are rendered as 0-length lines anyhow. Less links in array = faster animation.
      continue;
    }
    // 'links' are produced as 3 links+2 helper nodes; this is a generalized approach so we
    // can support multiple links between element nodes and/or groups, always, as each
    // 'original link' gets its own set of 2 helper nodes and thanks to the force layout
    // those helpers will all be in different places, hence the link 'path' for each
    // parallel link will be different.
    ui = nodeid(u);
    vi = nodeid(v);
    i = (ui < vi ? ui+"|"+vi : vi+"|"+ui);
    l = lm[i] || (lm[i] = {source:u, target:v, size:0, distance: 0});
    if (ustate == 1) {
      ui = rui;
    }
    if (vstate == 1) {
      vi = rvi;
    }
    ix = (ui < vi ? ui+"|"+vi+"|"+ustate+"|"+vstate : vi+"|"+ui+"|"+vstate+"|"+ustate);
    ix = (ui < vi ? ui+"|"+vi : vi+"|"+ui);
    // link(u,v) ==> u -> lu -> rv -> v
    lu = nml[ix] || (nml[ix] = data.helpers.left[ix]  || (data.helpers.left[ix]  = {ref: u, id: "_lh_" + ix, size: -1, link_ref: l}));
    rv = nmr[ix] || (nmr[ix] = data.helpers.right[ix] || (data.helpers.right[ix] = {ref: v, id: "_rh_" + ix, size: -1, link_ref: l}));
    uimg = nmimg[ui];
    vimg = nmimg[vi];
    ll = lml[ix] || (lml[ix] = {g_ref: l, ref: e, id: "l"+ix, source:uimg, target:  lu, real_source:u, real_target:v, size:0, distance: 0, left_seg  : true});
    l_ = lmm[ix] || (lmm[ix] = {g_ref: l, ref: e, id: "m"+ix, source:  lu, target:  rv, real_source:u, real_target:v, size:0, distance: 0, middle_seg: true});
    lr = lmr[ix] || (lmr[ix] = {g_ref: l, ref: e, id: "r"+ix, source:  rv, target:vimg, real_source:u, real_target:v, size:0, distance: 0, right_seg : true});
    l.size += 1;
    ll.size += 1;
    l_.size += 1;
    lr.size += 1;

    // these are only useful for single-linked nodes, but we don't care; here we have everything we need at minimum cost.
    if (l.size == 1) {
      u.link_count++;
      v.link_count++;
      u.first_link = l;
      v.first_link = l;
      u.first_link_target = v;
      v.first_link_target = u;
    }
  }

  for (k in lm) { links.push(lm[k]); }
  for (k in lml) { helper_links.push(lml[k]); }
  for (k in lmm) { helper_links.push(lmm[k]); helper_render_links.push(lmm[k]); }
  for (k in lmr) { helper_links.push(lmr[k]); }
  for (k in nml) { helper_nodes.push(nml[k]); }
  for (k in nmr) { helper_nodes.push(nmr[k]); }

  return {nodes: nodes, links: links, helper_nodes: helper_nodes, helper_links: helper_links, helper_render_links: helper_render_links};
}

function convexHulls(nodes, offset) {
  var hulls = {};

  // create point sets
  for (var k=0; k<nodes.length; ++k) {
    var n = nodes[k];
    if (n.size) continue;
    var i = getGroup(n),
        l = hulls[i] || (hulls[i] = []);
    l.push([n.x-offset, n.y-offset]);
    l.push([n.x-offset, n.y+offset]);
    l.push([n.x+offset, n.y-offset]);
    l.push([n.x+offset, n.y+offset]);
  }

  // create convex hulls
  var hullset = [];
  for (i in hulls) {
    hullset.push({group: i, path: d3.geom.hull(hulls[i])});
  }

  return hullset;
}

function drawCluster(d) {
  return curve(d.path); // 0.8
}

// these functions call init(); by declaring them here,
// they don't have the old init() as a closure any more.
// This should save us some memory and cycles when using
// this in a long-running setting.

function on_hull_click(d) {
  if (debug == 1) console.log("node click", d, arguments, this, expand[d.group]);
  // clicking on 'path helper nodes' shouln't expand/collapse the group node:
  if (d.size < 0)
    return;
  cycleState(d);
  init();
}

function on_node_click(d) {
  if (debug == 1) console.log("node click", d, arguments, this, expand[d.group]);
  // clicking on 'path helper nodes' shouln't expand/collapse the group node:
  if (d.size < 0)
    return;
  cycleState(d);
  init();
}

// --------------------------------------------------------

var body = d3.select("body");

var vis = body.append("svg")
   .attr("width", width)
   .attr("height", height);

var pathgen = d3.svg.line().interpolate("basis");

d3.json("miserables.json", function(json) {
  /*
  JSON layout:

  {
    "nodes": [
      {
        "name"  : "bla",    // in this code, this is expected to be a globally unique string (as it's used for the id via nodeid())
        "group" : 1         // group ID (number)
      },
      ...
    ],
    "links": [
      {
        "source" : 1,       // nodes[] index (number; is immediately converted to direct nodes[index] reference)
        "target" : 0,       // nodes[] index (number; is immediately converted to direct nodes[index] reference)
        "value"  : 1        // [not used in this force layout]
      },
      ...
    ]
  }
  */
  data = json;
  for (var i=0; i<data.links.length; ++i) {
    o = data.links[i];
    o.source = data.nodes[o.source];
    o.target = data.nodes[o.target];
  }
  // prepare data struct to also carry our 'path helper nodes':
  data.helpers = {left: {}, right: {}};

  hullg = vis.append("g");
  if (debug) {
    linkg = vis.append("g");
    helper_nodeg = vis.append("g");
  }
  helper_linkg = vis.append("g");
  nodeg = vis.append("g");
  if (debug == 1) {
    node = vis.append("g").append("circle")
        .attr("class", "center-of-mass")
        .attr("r", 10);
  }

  init();

  vis.attr("opacity", 1e-6)
    .transition()
    .duration(1000)
    .attr("opacity", 1);
});

function init() {
  /*
  We're kinda lazy with maintaining the anti-coll grid here: only when we hit a 'occupied' node,
  do we go and check if the occupier is still there, updating his quant grid location.

  This works because it 'evens out over time': a tested node hitting an 'unoccupied slot' takes that
  slot, so at the start, everybody might think they've got a free slot for themselves, then on the
  next 'tick', the slot may be suddenly found occupied by someone else also sitting in the same slot,
  causing double occupations to be resolved as the marked owner will stay, while all the others will
  be pushed out.

  As we'll have a lot of 'ticks' before the shows stops, we'll have plenty of time to get everybody
  to an actually really empty grid slot.

  Note that the feature set lists this as 'first come, first serve', but when you read this, I'm sure
  you realize that's a bit of a lie. After all, it's only really 'first come, first serve in nodes[]
  order' on the INITIAL ROUND, isn't it?
  */
  var anticollision_grid = [], xquant = 1, yquant = 1, xqthresh, yqthresh;

  if (force) force.stop();

  net = network(data, net);

  force = d3.layout.force()
      .nodes(net.nodes)
      .links(net.links)
      .size([width, height])
      .linkDistance(function(l, i) {
        //return 300;
        var n1 = l.source, n2 = l.target,
            g1 = n1.group_data || n1, g2 = n2.group_data || n2,
            n1_is_group = n1.size || 0, n2_is_group = n2.size || 0,
            rv = 300;
        // larger distance for bigger groups:
        // both between single nodes and _other_ groups (where size of own node group still counts),
        // and between two group nodes.
        //
        // reduce distance for groups with very few outer links,
        // again both in expanded and grouped form, i.e. between individual nodes of a group and
        // nodes of another group or other group node or between two group nodes.
        //
        // The latter was done to keep the single-link groups close.
        if (n1.group == n2.group) {
          if ((n1.link_count < 2 && !n1_is_group) || (n2.link_count < 2 && !n2_is_group)) {
            // 'real node' singles: these don't need a big distance to make the distance, if you whumsayin' ;-)
            rv = 2;
          } else if (!n1_is_group && !n2_is_group) {
            rv = 2;
          } else if (g1.link_count < 4 || g2.link_count < 4) {
            rv = 100;
          }
        } else {
          if (!n1_is_group && !n2_is_group) {
            rv = 50;
          } else if ((n1_is_group && n2_is_group) && (g1.link_count < 4 || g2.link_count < 4)) {
            // 'real node' singles: these don't need a big distance to make the ditance, if you whumsayin' ;-)
            rv = 100;
          } else if ((n1_is_group && g1.link_count < 2) || (n2_is_group && g2.link_count < 2)) {
            // 'real node' singles: these don't need a big distance to make the ditance, if you whumsayin' ;-)
            rv = 30;
          } else if (!n1_is_group || !n2_is_group) {
            rv = 100;
          }
        }
        return l.distance = rv;
      })
      .gravity(1.0)             // gravity+charge tweaked to ensure good 'grouped' view (e.g. green group not smack between blue&orange, ...
      .charge(function(d, i) {  // ... charge is important to turn single-linked groups to the outside
        if (d.size > 0) {
          return -5000;  // group node
        } else {
          // 'regular node'
          return -1000;
        }
      })
      .friction(0.7)   // friction adjusted to get dampened display: less bouncy bouncy ball [Swedish Chef, anyone?]
      .start();

  /*
  And here's the crazy idea for allowing AND rendering multiple links between 2 nodes, etc., as the initial attempt
  to include the 'helper' nodes in the basic 'force' failed dramatically from a visual PoV: we 'overlay' the basic
  nodes+links force with a SECOND force layout which 'augments' the original force layout by having it 'layout' all
  the helper nodes (with their links) between the 'fixed' REAL nodes, which are laid out by the original force.

  This way, we also have the freedom to apply a completely different force field setup to the helpers (no gravity
  as it doesn't make sense for helpers, different charge values, etc.).
  */
  force2 = d3.layout.force()
      .nodes(net.helper_nodes)
      .links(net.helper_links)
      .size([width, height])
      .linkDistance(function(l, i) {
        var n1 = l.real_source, n2 = l.real_target, rv,
            lr = l.g_ref,
            n1r, n2r,
            dx, dy;
        if (lr.source.size > 0 || lr.target.size > 0)
          return 20;
        return 1;
      })
      .gravity(0.0)   // just a tad of gravidy to help keep those curvy buttocks decent
      .charge(function(d, i) {
        // helper nodes have a medium-to-high charge, depending on the number of links the related force link represents.
        // Hence bundles of links fro A->B will have helper nodes with huge charges: better spreading of the link paths.
        //
        // Unless we're looking at helpers for links between 'real nodes', NOT GROUPS: in that case we want to keep
        // the lines are straight as posssible as there would only be one relation for A->B anyway, so we lower the charge
        // for such nodes and helpers.
        if (d.fixed)
          return -10;
        var l = d.link_ref,
            c = l.link_count || 1;
        if (l.source.size > 0 || l.target.size > 0)
          return -30;
        return -1;
      })
      .friction(0.95)
      .start()
      .stop();          // and immediately stop! force.tick will drive this one every tick!

  hullg.selectAll("path.hull").remove();
  hull = hullg.selectAll("path.hull")
      .data(convexHulls(net.nodes, off))
      .enter().append("path")
        .attr("class", "hull")
        .attr("d", drawCluster)
        .style("fill", function(d) { return fill(d.group); })
        .on("click", on_hull_click);

  if (debug == 1) {
    link = linkg.selectAll("line.link").data(net.links, linkid);
    link.exit().remove();
    link.enter().append("line")
        .attr("class", "link")
        .attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });
    // both existing and enter()ed links may have changed stroke width due to expand state change somewhere:
    link.style("stroke-width", function(d) { return d.size || 1; });
  }

  hlink = helper_linkg.selectAll("path.hlink").data(net.helper_render_links, function(d) {
    return d.id;
  });
  hlink.exit().remove();
  hlink.enter().append("path")
      .attr("class", "hlink");
  // both existing and enter()ed links may have changed stroke width due to expand state change somewhere:
  hlink.style("stroke-width", function(d) { return d.size || 1; });

  if (debug) {
    hnode = helper_nodeg.selectAll("circle.node").data(net.helper_nodes, function(d) {
      return d.id;
    });
    hnode.exit().remove();
    hnode.enter().append("circle")
        // if (d.size) -- d.size > 0 when d is a group node.
        // d.size < 0 when d is a 'path helper node'.
        .attr("class", function(d) {
          return "node" + (d.size > 0 ? "" : d.size < 0 ? " helper" : " leaf");
        })
        .attr("r", function(d) {
          return d.size > 0 ? d.size + dr : d.size < 0 ? 2 : dr + 1;
        })
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; })
        .style("fill", function(d) { return fill(d.group); });
  }

  node = nodeg.selectAll("circle.node").data(net.nodes, nodeid);
  node.exit().remove();
  node.enter().append("circle")
      // if (d.size) -- d.size > 0 when d is a group node.
      // d.size < 0 when d is a 'path helper node'.
      .attr("class", function(d) {
        return "node" + (d.size > 0 ? d.expansion ? " link-expanded" : "" : " leaf");
      })
      .attr("r", function(d) {
        return d.size > 0 ? d.size + dr : dr + 1;
      })
      .attr("cx", function(d) { return d.x; })
      .attr("cy", function(d) { return d.y; })
      .style("fill", function(d) { return fill(d.group); })
      .on("click", on_node_click);

  node.call(force.drag);

  var drag_in_progress = false;
  var change_squared;

  // CPU load redux for the fix, part 3: jumpstart the annealing process again when the user moves the mouse outside the node,
  // when we believe the drag is still going on; even when it isn't anymore, but D3 doesn't inform us about that!
  node
    .on("mouseout.ger_fix", function(d) {
      if (debug == 1) console.log("mouseout.ger_fix", this, arguments, d.fixed, drag_in_progress);
      if (drag_in_progress) {
        force.resume();
      }
    });

  var resume_threshold = 0.05;

  force.on("tick", function(e) {
    /*
    Force all nodes with only one link to point outwards.

    To do this, we first calculate the center mass (okay, we wing it, we fake node 'weight'),
    then see whether the target node for links from single-link nodes is closer to the
    center-of-mass than us, and if it isn't, we push the node outwards.
    */
    var center = {x: 0, y: 0, weight: 0}, singles = [],
        size, c, k, mx, my, dx, dy, alpha;

    drag_in_progress = false;
    net.nodes.forEach(function(n) {
      var w = Math.max(1, n.size || 0, n.weight || 0);

      center.x += w * n.x;
      center.y += w * n.y;
      center.weight += w;

      if (n.fixed & 2) {
        drag_in_progress = true;
      }

      if (n.size > 0 ? n.link_count < 4 : n.group_data.link_count < 3)
        singles.push(n);
    });

    size = force.size();

    mx = size[0] / 2;
    my = size[1] / 2;

    singles.forEach(function(n) {
      var l = n.first_link, n2 = n.first_link_target,
          proj, ax, bx, ay, by, k, x, y, alpha, rej, power,
          dx, dy,
          n_is_group = n.size || 0,
          ng = n.group_data || n,
          c2,
          w = Math.max(1, n.size || 0, n.weight || 0);

      // haven't decided what to do for unconnected nodes, yet...
      if (!l)
        return;

      // apply amplification of the 'original' alpha:
      // 1.0 for singles and double-connected nodes, close to 0 for highly connected nodes, rapidly decreasing.
      // Use this as we want to give those 'non-singles' a little bit of the same 'push out' treatment.
      // Reduce effect for 'real nodes' which are singles: they need much less encouragement!
      power = Math.max(2, n_is_group ? n.link_count : n.group_data.link_count);
      power = 2 / power;

      alpha = e.alpha * power;

      // undo/revert gravity forces (or as near as we can get, here)
      //
      // revert for truely single nodes, revert just a wee little bit for dual linked nodes,
      // only reduce ever so slighty for nodes with few links (~ 3) that made it into this
      // 'singles' selection
      if (k = alpha * force.gravity() * (0.8 + power)) {
        dx = (mx - n.x) * k;
        dy = (my - n.y) * k;
        n.x -= dx;
        n.y -= dy;

        center.x -= dx * w;
        center.y -= dy * w;
      }
    });

    // move the entire graph so that its center of mass sits at the center, period.
    center.x /= center.weight;
    center.y /= center.weight;

    if (debug == 1) {
      c = vis.selectAll("circle.center-of-mass")
          .attr("cx", center.x)
          .attr("cy", center.y);
    }

    dx = mx - center.x;
    dy = my - center.y;

    alpha = e.alpha * 5;
    dx *= alpha;
    dy *= alpha;

    net.nodes.forEach(function(n) {
      n.x += dx;
      n.y += dy;
    });


    change_squared = 0;

    // fixup .px/.py so drag behaviour and annealing get the correct values, as
    // force.tick() would expect .px and .py to be the .x and .y of yesterday.
    net.nodes.forEach(function(n) {
      // restrain all nodes to window area
      var k, dx, dy,
          r = (n.size > 0 ? n.size + dr : dr + 1) + 2 /* styled border outer thickness and a bit */;

      dx = 0;
      if (n.x < r)
        dx = r - n.x;
      else if (n.x > size[0] - r)
        dx = size[0] - r - n.x;

      dy = 0;
      if (n.y < r)
        dy = r - n.y;
      else if (n.y > size[1] - r)
        dy = size[1] - r - n.y;

      k = 1.2;

      n.x += dx * k;
      n.y += dy * k;
      // restraining completed.......................

      // fixes 'elusive' node behaviour when hovering with the mouse (related to force.drag)
      if (n.fixed) {
        // 'elusive behaviour' ~ move mouse near node and node would take off, i.e. act as an elusive creature.
        n.x = n.px;
        n.y = n.py;
      }
      n.px = n.x;
      n.py = n.y;

      // plus copy for faster stop check
      change_squared += (n.qx - n.x) * (n.qx - n.x);
      change_squared += (n.qy - n.y) * (n.qy - n.y);
      n.qx = n.x;
      n.qy = n.y;
    });

    // kick the force2 to also do a bit of annealing alongside:
    // to make it do something, we need to surround it alpha-tweaking stuff, though.
    force2.resume();
    force2.tick();
    force2.stop();

    // fast stop + the drag fix, part 2:
    if (change_squared < .005) {
      if (debug == 1) console.log("fast stop: CPU load redux");
      force.stop();
      // fix part 4: monitor D3 resetting the drag marker:
      if (drag_in_progress) {
        if (debug == 1) console.log("START monitor drag in progress", drag_in_progress);
        d3.timer(function() {
          drag_in_progress = false;
          net.nodes.forEach(function(n) {
            if (n.fixed & 2) {
              drag_in_progress = true;
            }
          });
          force.resume();
          if (debug == 1) console.log("monitor drag in progress: drag ENDED", drag_in_progress);
          // Quit monitoring as soon as we noticed the drag ENDED.
          // Note: we continue to monitor at +500ms intervals beyond the last tick
          //       as this timer function ALWAYS kickstarts the force layout again
          //       through force.resume().
          //       d3.timer() API only accepts an initial delay; we can't set this
          //       thing to scan, say, every 500msecs until the drag is done,
          //       so we do it that way, via the revived force.tick process.
          return true;
        }, 500);
      }
    } else if (change_squared > net.nodes.length * 5 && e.alpha < resume_threshold) {
      // jolt the alpha (and the visual) when there's still a lot of change when we hit the alpha threshold.
      force.alpha(Math.min(0.1, e.alpha *= 2)); //force.resume(), but now with decreasing alpha starting value so the jolts don't get so big.

      // And 'dampen out' the trigger point, so it becomes harder and harder to trigger the threshold.
      // This is done to cope with those instable (forever rotating, etc.) layouts...
      resume_threshold *= 0.9;
    }

    //--------------------------------------------------------------------

    if (!hull.empty()) {
      hull.data(convexHulls(net.nodes, off))
          .attr("d", drawCluster);
    }

    if (debug == 1) {
      link.attr("x1", function(d) { return d.source.x; })
          .attr("y1", function(d) { return d.source.y; })
          .attr("x2", function(d) { return d.target.x; })
          .attr("y2", function(d) { return d.target.y; });
    }

    node.attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
  });

  force2.on("tick", function(e) {
    /*
      Update all 'real'=fixed nodes.
    */
    net.helper_nodes.forEach(function(n) {
      var o;
      if (n.fixed) {
        o = n.ref;
        n.px = n.x = o.x;
        n.py = n.y = o.y;
      }
    });
    net.helper_links.forEach(function(l) {
      var o = l.g_ref;
      l.distance = o.distance;
    });

    // NOTE: force2 is fully driven by force(1), but still there's need for 'fast stop' handling in here
    //       as our force2 may be more 'joyous' in animating the links that force is animating the nodes
    //       themselves. Hence we also take the delta movement of the helper nodes into account!
    net.helper_nodes.forEach(function(n) {
      // skip the 'fixed' buggers: those are already accounted for in force.tick!
      if (n.fixed)
        return;

      // plus copy for faster stop check
      change_squared += (n.qx - n.x) * (n.qx - n.x);
      change_squared += (n.qy - n.y) * (n.qy - n.y);
      n.qx = n.x;
      n.qy = n.y;
    });

    //--------------------------------------------------------------------

    hlink.attr("d", function(d) {
      var linedata = [
          [d.real_source.x, d.real_source.y],
          [d.source.x, d.source.y],
          [d.target.x, d.target.y],
          [d.real_target.x, d.real_target.y]
      ];
      return pathgen(linedata);
    });

    if (debug) {
      hnode.attr("cx", function(d) { return d.x; })
           .attr("cy", function(d) { return d.y; });
    }
  });
}

    </script>
  </body>
</html>
{"nodes":[{"name":"Myriel","group":1},{"name":"Napoleon","group":1},{"name":"Mlle.Baptistine","group":1},{"name":"Mme.Magloire","group":1},{"name":"CountessdeLo","group":1},{"name":"Geborand","group":1},{"name":"Champtercier","group":1},{"name":"Cravatte","group":1},{"name":"Count","group":1},{"name":"OldMan","group":1},{"name":"Labarre","group":2},{"name":"Valjean","group":2},{"name":"Marguerite","group":3},{"name":"Mme.deR","group":2},{"name":"Isabeau","group":2},{"name":"Gervais","group":2},{"name":"Tholomyes","group":3},{"name":"Listolier","group":3},{"name":"Fameuil","group":3},{"name":"Blacheville","group":3},{"name":"Favourite","group":3},{"name":"Dahlia","group":3},{"name":"Zephine","group":3},{"name":"Fantine","group":3},{"name":"Mme.Thenardier","group":4},{"name":"Thenardier","group":4},{"name":"Cosette","group":5},{"name":"Javert","group":4},{"name":"Fauchelevent","group":0},{"name":"Bamatabois","group":2},{"name":"Perpetue","group":3},{"name":"Simplice","group":2},{"name":"Scaufflaire","group":2},{"name":"Woman1","group":2},{"name":"Judge","group":2},{"name":"Champmathieu","group":2},{"name":"Brevet","group":2},{"name":"Chenildieu","group":2},{"name":"Cochepaille","group":2},{"name":"Pontmercy","group":4},{"name":"Boulatruelle","group":6},{"name":"Eponine","group":4},{"name":"Anzelma","group":4},{"name":"Woman2","group":5},{"name":"MotherInnocent","group":0},{"name":"Gribier","group":0},{"name":"Jondrette","group":7},{"name":"Mme.Burgon","group":7},{"name":"Gavroche","group":8},{"name":"Gillenormand","group":5},{"name":"Magnon","group":5},{"name":"Mlle.Gillenormand","group":5},{"name":"Mme.Pontmercy","group":5},{"name":"Mlle.Vaubois","group":5},{"name":"Lt.Gillenormand","group":5},{"name":"Marius","group":8},{"name":"BaronessT","group":5},{"name":"Mabeuf","group":8},{"name":"Enjolras","group":8},{"name":"Combeferre","group":8},{"name":"Prouvaire","group":8},{"name":"Feuilly","group":8},{"name":"Courfeyrac","group":8},{"name":"Bahorel","group":8},{"name":"Bossuet","group":8},{"name":"Joly","group":8},{"name":"Grantaire","group":8},{"name":"MotherPlutarch","group":9},{"name":"Gueulemer","group":4},{"name":"Babet","group":4},{"name":"Claquesous","group":4},{"name":"Montparnasse","group":4},{"name":"Toussaint","group":5},{"name":"Child1","group":10},{"name":"Child2","group":10},{"name":"Brujon","group":4},{"name":"Mme.Hucheloup","group":8}],"links":[{"source":1,"target":0,"value":1},{"source":2,"target":0,"value":8},{"source":3,"target":0,"value":10},{"source":3,"target":2,"value":6},{"source":4,"target":0,"value":1},{"source":5,"target":0,"value":1},{"source":6,"target":0,"value":1},{"source":7,"target":0,"value":1},{"source":8,"target":0,"value":2},{"source":9,"target":0,"value":1},{"source":11,"target":10,"value":1},{"source":11,"target":3,"value":3},{"source":11,"target":2,"value":3},{"source":11,"target":0,"value":5},{"source":12,"target":11,"value":1},{"source":13,"target":11,"value":1},{"source":14,"target":11,"value":1},{"source":15,"target":11,"value":1},{"source":17,"target":16,"value":4},{"source":18,"target":16,"value":4},{"source":18,"target":17,"value":4},{"source":19,"target":16,"value":4},{"source":19,"target":17,"value":4},{"source":19,"target":18,"value":4},{"source":20,"target":16,"value":3},{"source":20,"target":17,"value":3},{"source":20,"target":18,"value":3},{"source":20,"target":19,"value":4},{"source":21,"target":16,"value":3},{"source":21,"target":17,"value":3},{"source":21,"target":18,"value":3},{"source":21,"target":19,"value":3},{"source":21,"target":20,"value":5},{"source":22,"target":16,"value":3},{"source":22,"target":17,"value":3},{"source":22,"target":18,"value":3},{"source":22,"target":19,"value":3},{"source":22,"target":20,"value":4},{"source":22,"target":21,"value":4},{"source":23,"target":16,"value":3},{"source":23,"target":17,"value":3},{"source":23,"target":18,"value":3},{"source":23,"target":19,"value":3},{"source":23,"target":20,"value":4},{"source":23,"target":21,"value":4},{"source":23,"target":22,"value":4},{"source":23,"target":12,"value":2},{"source":23,"target":11,"value":9},{"source":24,"target":23,"value":2},{"source":24,"target":11,"value":7},{"source":25,"target":24,"value":13},{"source":25,"target":23,"value":1},{"source":25,"target":11,"value":12},{"source":26,"target":24,"value":4},{"source":26,"target":11,"value":31},{"source":26,"target":16,"value":1},{"source":26,"target":25,"value":1},{"source":27,"target":11,"value":17},{"source":27,"target":23,"value":5},{"source":27,"target":25,"value":5},{"source":27,"target":24,"value":1},{"source":27,"target":26,"value":1},{"source":28,"target":11,"value":8},{"source":28,"target":27,"value":1},{"source":29,"target":23,"value":1},{"source":29,"target":27,"value":1},{"source":29,"target":11,"value":2},{"source":30,"target":23,"value":1},{"source":31,"target":30,"value":2},{"source":31,"target":11,"value":3},{"source":31,"target":23,"value":2},{"source":31,"target":27,"value":1},{"source":32,"target":11,"value":1},{"source":33,"target":11,"value":2},{"source":33,"target":27,"value":1},{"source":34,"target":11,"value":3},{"source":34,"target":29,"value":2},{"source":35,"target":11,"value":3},{"source":35,"target":34,"value":3},{"source":35,"target":29,"value":2},{"source":36,"target":34,"value":2},{"source":36,"target":35,"value":2},{"source":36,"target":11,"value":2},{"source":36,"target":29,"value":1},{"source":37,"target":34,"value":2},{"source":37,"target":35,"value":2},{"source":37,"target":36,"value":2},{"source":37,"target":11,"value":2},{"source":37,"target":29,"value":1},{"source":38,"target":34,"value":2},{"source":38,"target":35,"value":2},{"source":38,"target":36,"value":2},{"source":38,"target":37,"value":2},{"source":38,"target":11,"value":2},{"source":38,"target":29,"value":1},{"source":39,"target":25,"value":1},{"source":40,"target":25,"value":1},{"source":41,"target":24,"value":2},{"source":41,"target":25,"value":3},{"source":42,"target":41,"value":2},{"source":42,"target":25,"value":2},{"source":42,"target":24,"value":1},{"source":43,"target":11,"value":3},{"source":43,"target":26,"value":1},{"source":43,"target":27,"value":1},{"source":44,"target":28,"value":3},{"source":44,"target":11,"value":1},{"source":45,"target":28,"value":2},{"source":47,"target":46,"value":1},{"source":48,"target":47,"value":2},{"source":48,"target":25,"value":1},{"source":48,"target":27,"value":1},{"source":48,"target":11,"value":1},{"source":49,"target":26,"value":3},{"source":49,"target":11,"value":2},{"source":50,"target":49,"value":1},{"source":50,"target":24,"value":1},{"source":51,"target":49,"value":9},{"source":51,"target":26,"value":2},{"source":51,"target":11,"value":2},{"source":52,"target":51,"value":1},{"source":52,"target":39,"value":1},{"source":53,"target":51,"value":1},{"source":54,"target":51,"value":2},{"source":54,"target":49,"value":1},{"source":54,"target":26,"value":1},{"source":55,"target":51,"value":6},{"source":55,"target":49,"value":12},{"source":55,"target":39,"value":1},{"source":55,"target":54,"value":1},{"source":55,"target":26,"value":21},{"source":55,"target":11,"value":19},{"source":55,"target":16,"value":1},{"source":55,"target":25,"value":2},{"source":55,"target":41,"value":5},{"source":55,"target":48,"value":4},{"source":56,"target":49,"value":1},{"source":56,"target":55,"value":1},{"source":57,"target":55,"value":1},{"source":57,"target":41,"value":1},{"source":57,"target":48,"value":1},{"source":58,"target":55,"value":7},{"source":58,"target":48,"value":7},{"source":58,"target":27,"value":6},{"source":58,"target":57,"value":1},{"source":58,"target":11,"value":4},{"source":59,"target":58,"value":15},{"source":59,"target":55,"value":5},{"source":59,"target":48,"value":6},{"source":59,"target":57,"value":2},{"source":60,"target":48,"value":1},{"source":60,"target":58,"value":4},{"source":60,"target":59,"value":2},{"source":61,"target":48,"value":2},{"source":61,"target":58,"value":6},{"source":61,"target":60,"value":2},{"source":61,"target":59,"value":5},{"source":61,"target":57,"value":1},{"source":61,"target":55,"value":1},{"source":62,"target":55,"value":9},{"source":62,"target":58,"value":17},{"source":62,"target":59,"value":13},{"source":62,"target":48,"value":7},{"source":62,"target":57,"value":2},{"source":62,"target":41,"value":1},{"source":62,"target":61,"value":6},{"source":62,"target":60,"value":3},{"source":63,"target":59,"value":5},{"source":63,"target":48,"value":5},{"source":63,"target":62,"value":6},{"source":63,"target":57,"value":2},{"source":63,"target":58,"value":4},{"source":63,"target":61,"value":3},{"source":63,"target":60,"value":2},{"source":63,"target":55,"value":1},{"source":64,"target":55,"value":5},{"source":64,"target":62,"value":12},{"source":64,"target":48,"value":5},{"source":64,"target":63,"value":4},{"source":64,"target":58,"value":10},{"source":64,"target":61,"value":6},{"source":64,"target":60,"value":2},{"source":64,"target":59,"value":9},{"source":64,"target":57,"value":1},{"source":64,"target":11,"value":1},{"source":65,"target":63,"value":5},{"source":65,"target":64,"value":7},{"source":65,"target":48,"value":3},{"source":65,"target":62,"value":5},{"source":65,"target":58,"value":5},{"source":65,"target":61,"value":5},{"source":65,"target":60,"value":2},{"source":65,"target":59,"value":5},{"source":65,"target":57,"value":1},{"source":65,"target":55,"value":2},{"source":66,"target":64,"value":3},{"source":66,"target":58,"value":3},{"source":66,"target":59,"value":1},{"source":66,"target":62,"value":2},{"source":66,"target":65,"value":2},{"source":66,"target":48,"value":1},{"source":66,"target":63,"value":1},{"source":66,"target":61,"value":1},{"source":66,"target":60,"value":1},{"source":67,"target":57,"value":3},{"source":68,"target":25,"value":5},{"source":68,"target":11,"value":1},{"source":68,"target":24,"value":1},{"source":68,"target":27,"value":1},{"source":68,"target":48,"value":1},{"source":68,"target":41,"value":1},{"source":69,"target":25,"value":6},{"source":69,"target":68,"value":6},{"source":69,"target":11,"value":1},{"source":69,"target":24,"value":1},{"source":69,"target":27,"value":2},{"source":69,"target":48,"value":1},{"source":69,"target":41,"value":1},{"source":70,"target":25,"value":4},{"source":70,"target":69,"value":4},{"source":70,"target":68,"value":4},{"source":70,"target":11,"value":1},{"source":70,"target":24,"value":1},{"source":70,"target":27,"value":1},{"source":70,"target":41,"value":1},{"source":70,"target":58,"value":1},{"source":71,"target":27,"value":1},{"source":71,"target":69,"value":2},{"source":71,"target":68,"value":2},{"source":71,"target":70,"value":2},{"source":71,"target":11,"value":1},{"source":71,"target":48,"value":1},{"source":71,"target":41,"value":1},{"source":71,"target":25,"value":1},{"source":72,"target":26,"value":2},{"source":72,"target":27,"value":1},{"source":72,"target":11,"value":1},{"source":73,"target":48,"value":2},{"source":74,"target":48,"value":2},{"source":74,"target":73,"value":3},{"source":75,"target":69,"value":3},{"source":75,"target":68,"value":3},{"source":75,"target":25,"value":3},{"source":75,"target":48,"value":1},{"source":75,"target":41,"value":1},{"source":75,"target":70,"value":1},{"source":75,"target":71,"value":1},{"source":76,"target":64,"value":1},{"source":76,"target":65,"value":1},{"source":76,"target":66,"value":1},{"source":76,"target":63,"value":1},{"source":76,"target":62,"value":1},{"source":76,"target":48,"value":1},{"source":76,"target":58,"value":1}]}