source: Dev/branches/jos-branch/d3/src/layout/force.js @ 230

Last change on this file since 230 was 76, checked in by fpvanagthoven, 14 years ago

d3

File size: 9.1 KB
Line 
1// A rudimentary force layout using Gauss-Seidel.
2d3.layout.force = function() {
3  var force = {},
4      event = d3.dispatch("tick"),
5      size = [1, 1],
6      alpha,
7      friction = .9,
8      linkDistance = d3_layout_forceLinkDistance,
9      linkStrength = d3_layout_forceLinkStrength,
10      charge = -30,
11      gravity = .1,
12      theta = .8,
13      interval,
14      nodes = [],
15      links = [],
16      distances,
17      strengths;
18
19  function repulse(node, kc) {
20    return function(quad, x1, y1, x2, y2) {
21      if (quad.point !== node) {
22        var dx = quad.cx - node.x,
23            dy = quad.cy - node.y,
24            dn = 1 / Math.sqrt(dx * dx + dy * dy);
25
26        /* Barnes-Hut criterion. */
27        if ((x2 - x1) * dn < theta) {
28          var k = kc * quad.count * dn * dn;
29          node.x += dx * k;
30          node.y += dy * k;
31          return true;
32        }
33
34        if (quad.point && isFinite(dn)) {
35          var k = kc * dn * dn;
36          node.x += dx * k;
37          node.y += dy * k;
38        }
39      }
40    };
41  }
42
43  function tick() {
44    var n = nodes.length,
45        m = links.length,
46        q = d3.geom.quadtree(nodes),
47        i, // current index
48        o, // current object
49        s, // current source
50        t, // current target
51        l, // current distance
52        x, // x-distance
53        y; // y-distance
54
55    // gauss-seidel relaxation for links
56    for (i = 0; i < m; ++i) {
57      o = links[i];
58      s = o.source;
59      t = o.target;
60      x = t.x - s.x;
61      y = t.y - s.y;
62      if (l = (x * x + y * y)) {
63        l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l;
64        x *= l;
65        y *= l;
66        t.x -= x;
67        t.y -= y;
68        s.x += x;
69        s.y += y;
70      }
71    }
72
73    // apply gravity forces
74    var kg = alpha * gravity;
75    x = size[0] / 2;
76    y = size[1] / 2;
77    i = -1; while (++i < n) {
78      o = nodes[i];
79      o.x += (x - o.x) * kg;
80      o.y += (y - o.y) * kg;
81    }
82
83    // compute quadtree center of mass
84    d3_layout_forceAccumulate(q);
85
86    // apply charge forces
87    var kc = alpha * charge;
88    i = -1; while (++i < n) {
89      q.visit(repulse(nodes[i], kc));
90    }
91
92    // position verlet integration
93    i = -1; while (++i < n) {
94      o = nodes[i];
95      if (o.fixed) {
96        o.x = o.px;
97        o.y = o.py;
98      } else {
99        o.x -= (o.px - (o.px = o.x)) * friction;
100        o.y -= (o.py - (o.py = o.y)) * friction;
101      }
102    }
103
104    event.tick.dispatch({type: "tick", alpha: alpha});
105
106    // simulated annealing, basically
107    return (alpha *= .99) < .005;
108  }
109
110  force.on = function(type, listener) {
111    event[type].add(listener);
112    return force;
113  };
114
115  force.nodes = function(x) {
116    if (!arguments.length) return nodes;
117    nodes = x;
118    return force;
119  };
120
121  force.links = function(x) {
122    if (!arguments.length) return links;
123    links = x;
124    return force;
125  };
126
127  force.size = function(x) {
128    if (!arguments.length) return size;
129    size = x;
130    return force;
131  };
132
133  force.linkDistance = function(x) {
134    if (!arguments.length) return linkDistance;
135    linkDistance = d3.functor(x);
136    return force;
137  };
138
139  // For backwards-compatibility.
140  force.distance = force.linkDistance;
141
142  force.linkStrength = function(x) {
143    if (!arguments.length) return linkStrength;
144    linkStrength = d3.functor(x);
145    return force;
146  };
147
148  force.friction = function(x) {
149    if (!arguments.length) return friction;
150    friction = x;
151    return force;
152  };
153
154  force.charge = function(x) {
155    if (!arguments.length) return charge;
156    charge = x;
157    return force;
158  };
159
160  force.gravity = function(x) {
161    if (!arguments.length) return gravity;
162    gravity = x;
163    return force;
164  };
165
166  force.theta = function(x) {
167    if (!arguments.length) return theta;
168    theta = x;
169    return force;
170  };
171
172  force.start = function() {
173    var i,
174        j,
175        n = nodes.length,
176        m = links.length,
177        w = size[0],
178        h = size[1],
179        neighbors,
180        o;
181
182    for (i = 0; i < n; ++i) {
183      (o = nodes[i]).index = i;
184    }
185
186    distances = [];
187    strengths = [];
188    for (i = 0; i < m; ++i) {
189      o = links[i];
190      if (typeof o.source == "number") o.source = nodes[o.source];
191      if (typeof o.target == "number") o.target = nodes[o.target];
192      distances[i] = linkDistance.call(this, o, i);
193      strengths[i] = linkStrength.call(this, o, i);
194    }
195
196    for (i = 0; i < n; ++i) {
197      o = nodes[i];
198      if (isNaN(o.x)) o.x = position("x", w);
199      if (isNaN(o.y)) o.y = position("y", h);
200      if (isNaN(o.px)) o.px = o.x;
201      if (isNaN(o.py)) o.py = o.y;
202    }
203
204    // initialize node position based on first neighbor
205    function position(dimension, size) {
206      var neighbors = neighbor(i),
207          j = -1,
208          m = neighbors.length,
209          x;
210      while (++j < m) if (!isNaN(x = neighbors[j][dimension])) return x;
211      return Math.random() * size;
212    }
213
214    // initialize neighbors lazily
215    function neighbor() {
216      if (!neighbors) {
217        neighbors = [];
218        for (j = 0; j < n; ++j) {
219          neighbors[j] = [];
220        }
221        for (j = 0; j < m; ++j) {
222          var o = links[j];
223          neighbors[o.source.index].push(o.target);
224          neighbors[o.target.index].push(o.source);
225        }
226      }
227      return neighbors[i];
228    }
229
230    return force.resume();
231  };
232
233  force.resume = function() {
234    alpha = .1;
235    d3.timer(tick);
236    return force;
237  };
238
239  force.stop = function() {
240    alpha = 0;
241    return force;
242  };
243
244  // use `node.call(force.drag)` to make nodes draggable
245  force.drag = function() {
246
247    this
248      .on("mouseover.force", d3_layout_forceDragOver)
249      .on("mouseout.force", d3_layout_forceDragOut)
250      .on("mousedown.force", dragdown)
251      .on("touchstart.force", dragdown);
252
253    d3.select(window)
254      .on("mousemove.force", d3_layout_forceDragMove)
255      .on("touchmove.force", d3_layout_forceDragMove)
256      .on("mouseup.force", d3_layout_forceDragUp, true)
257      .on("touchend.force", d3_layout_forceDragUp, true)
258      .on("click.force", d3_layout_forceDragClick, true);
259
260    return force;
261  };
262
263  function dragdown(d, i) {
264    var m = d3_layout_forcePoint(this.parentNode);
265    (d3_layout_forceDragNode = d).fixed = true;
266    d3_layout_forceDragMoved = false;
267    d3_layout_forceDragElement = this;
268    d3_layout_forceDragForce = force;
269    d3_layout_forceDragOffset = [m[0] - d.x, m[1] - d.y];
270    d3_layout_forceCancel();
271  }
272
273  return force;
274};
275
276var d3_layout_forceDragForce,
277    d3_layout_forceDragNode,
278    d3_layout_forceDragMoved,
279    d3_layout_forceDragOffset,
280    d3_layout_forceStopClick,
281    d3_layout_forceDragElement;
282
283function d3_layout_forceDragOver(d) {
284  d.fixed = true;
285}
286
287function d3_layout_forceDragOut(d) {
288  if (d !== d3_layout_forceDragNode) {
289    d.fixed = false;
290  }
291}
292
293function d3_layout_forcePoint(container) {
294  return d3.event.touches
295      ? d3.svg.touches(container)[0]
296      : d3.svg.mouse(container);
297}
298
299function d3_layout_forceDragMove() {
300  if (!d3_layout_forceDragNode) return;
301  var parent = d3_layout_forceDragElement.parentNode;
302
303  // O NOES! The drag element was removed from the DOM.
304  if (!parent) {
305    d3_layout_forceDragNode.fixed = false;
306    d3_layout_forceDragOffset = d3_layout_forceDragNode = d3_layout_forceDragElement = null;
307    return;
308  }
309
310  var m = d3_layout_forcePoint(parent);
311  d3_layout_forceDragMoved = true;
312  d3_layout_forceDragNode.px = m[0] - d3_layout_forceDragOffset[0];
313  d3_layout_forceDragNode.py = m[1] - d3_layout_forceDragOffset[1];
314  d3_layout_forceCancel();
315  d3_layout_forceDragForce.resume(); // restart annealing
316}
317
318function d3_layout_forceDragUp() {
319  if (!d3_layout_forceDragNode) return;
320
321  // If the node was moved, prevent the mouseup from propagating.
322  // Also prevent the subsequent click from propagating (e.g., for anchors).
323  if (d3_layout_forceDragMoved) {
324    d3_layout_forceStopClick = true;
325    d3_layout_forceCancel();
326  }
327
328  // Don't trigger this for touchend.
329  if (d3.event.type === "mouseup") {
330    d3_layout_forceDragMove();
331  }
332
333  d3_layout_forceDragNode.fixed = false;
334  d3_layout_forceDragForce =
335  d3_layout_forceDragOffset =
336  d3_layout_forceDragNode =
337  d3_layout_forceDragElement = null;
338}
339
340function d3_layout_forceDragClick() {
341  if (d3_layout_forceStopClick) {
342    d3_layout_forceCancel();
343    d3_layout_forceStopClick = false;
344  }
345}
346
347function d3_layout_forceCancel() {
348  d3.event.stopPropagation();
349  d3.event.preventDefault();
350}
351
352function d3_layout_forceAccumulate(quad) {
353  var cx = 0,
354      cy = 0;
355  quad.count = 0;
356  if (!quad.leaf) {
357    var nodes = quad.nodes,
358        n = nodes.length,
359        i = -1,
360        c;
361    while (++i < n) {
362      c = nodes[i];
363      if (c == null) continue;
364      d3_layout_forceAccumulate(c);
365      quad.count += c.count;
366      cx += c.count * c.cx;
367      cy += c.count * c.cy;
368    }
369  }
370  if (quad.point) {
371    // jitter internal nodes that are coincident
372    if (!quad.leaf) {
373      quad.point.x += Math.random() - .5;
374      quad.point.y += Math.random() - .5;
375    }
376    quad.count++;
377    cx += quad.point.x;
378    cy += quad.point.y;
379  }
380  quad.cx = cx / quad.count;
381  quad.cy = cy / quad.count;
382}
383
384function d3_layout_forceLinkDistance(link) {
385  return 20;
386}
387
388function d3_layout_forceLinkStrength(link) {
389  return 1;
390}
Note: See TracBrowser for help on using the repository browser.