This plugin provides ellipseForce
, an alternative for components manyBodies
and collision
in d3-force
-module. EllipseForce can be used to create force-directed graph layouts where nodes are ellipses or unequal rectangles, e.g. labels or text snippets, which often require wide and low rectangles.
Though ellipseForce calculates elliptic forces, the graphical representation of the node can be a rectangle: Force-directed graphs don't generally work well if nodes are calculated as straight-angled objects -- elliptic shapes allow nodes to nudge and slide past each others in a way that is worth the risk of slight overlap in corners.
Typically force directed graphs treat nodes as circles that 'radiate' their force equally to all directions. EllipseForce changes the shape of this repulsion to be ellipse. Assume that our node is shaped as a wide and low ellipse. Now the repulsion is stronger when the other object is on the same level with the ellipse, on the longer radius and gets weaker as the object rotates towards the shorter radius. Same with the overlaps, if the overlap is happening on the longer radius of the ellipse, then the other object is pushed harder to that direction.
See comments in src/ellipseForce.js for further explanation. The code aims for readability, this is not intended to be a library to be maintained and perfected, this is more to provide a commented solution that you should modify and improve for your use.
In d3 v.4, force calculations are divided into components, where one component can be force to push nodes away from each other (manyBodies
), another to make sure that they don't overlap (collision
) and one to make links pull nodes together (links
). Like many older force layout algorithms, ellipseForce
treats pushing apart and avoiding overlaps as same task, thus replacing both manyBodies
and collision
. You can still use other d3-force components with it.
EllipseForce uses simulation's alpha, and will relax the tension in time like other forces.
A price we have to pay for better layout for labels is performance. EllipseForce doesn't use quadtrees, and as such doesn't scale for hundreds of nodes. It is developed in a hurry and I don't have the time or capacity to think if quadtrees would work at all and how should they be used with elliptic nodes.
If you use NPM, npm install d3-ellipse-force
. But really, it won't install any dependencies, because the single function provided by this plugin doesn't have any strict dependencies. It is assumed to be used as force in d3-force
simulation, and to actually draw things with d3, you'll need several other d3-modules. NPM package is mainly there to help bundling ellipseForce into a more complex project.
You can download the latest release.
The easiest way to get started with ellipseForce is to download the latest release, put the d3-ellipse-force.js or its minified version into same folder with your html file, and use minified, external d3-library to provide you with d3-force and the rest:
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="d3-ellipse-force.js"></script>
Use ellipseForce as force in d3's forceSimulation.
# ellipseForce([padding, innerRepulse, outerRepulse])
ellipseForce can take three parameters, padding(=4) innerRepulse(=.5) and outerRepulse(=.5).
padding
is invisible padding added to radii of ellipsi, a reliable way to add space between nodes.innerRepulse
is used to calculate the strength of repulsion when nodes are inside each other, aka. overlap.outerRepulse
is the base strength of repulsion towards other, non-overlapping nodes. It dissipates fast.
# force.padding([my_padding]) <>
If padding is specified, sets the current padding to the specified size and returns the force. padding can be given a function that is called with the current node as argument to dynamically create the padding. If padding is not specified, returns the function to receive the current padding size, which defaults to function that returns 4, when called (with any arguments).
# force.innerRepulse([my_innerRepulse]) <>
If innerRepulse is specified, sets the repulsion for overlapping nodes to the specified amount and returns the force. If innerRepulse is not specified, returns the current repulsion, which defaults to 0.5.
# force.outerRepulse([my_outerRepulse]) <>
If outerRepulse is specified, sets the repulsion for non-overlapping nodes to the specified amount and returns the force. If outerRepulse is not specified, returns the current repulsion, which defaults to 0.5.
Example for including ellipseForce as a force (compare to forceSimulation):
var simulation = d3.forceSimulation(nodes)
.force("charge", d3.ellipseForce())
.force("link", d3.forceLink(links))
.force("center", d3.forceCenter());
I have struggled with rectangular nodes and force-directed graphs several times while developing Kataja, visualisation tool for biolinguistics. It would be useful to have a dynamic algorithm for placing those syntactic elements that don't have one fixed place in a tree. When nodes are snippets of text of various length, common algorithms either reserve too much or too little space for each of them. My many attempts to modify the algorithms to understand rectangles resulted in jumpy graphs that didn't settle nicely. These ellipses are the final, best result. It seems that other people in visualisation have experienced similar problems with force-directed algorithms, so I decided to let the solution fly as a d3 plugin.