A powerful component in the D3 library is the Force library d3-force. You can find some stunning animations on the internet created with d3-force. Especially for people into physics who would like to visualize the interactions between small particles or large bodies governed by laws of attraction and and acceleration, the d3-force library is quite attractive.
It took me a while to understand what d3-force is and also what it is not. Let me try to explain:
- d3-force can calculate the position and velocity of elements in a system governed by rules resembling laws of physics, such as the law of gravity or other interaction forces or force fields
- d3-force can be used to determine for elements with a size, mass, initial position and speed their location and velocity (at any given moment or after a certain time) – for example to find the stable end-state (if it exists).
- d3-force does not act on SVG elements; d3-force acts on data
- we can take the results from the calculation (also called simulation) of d3-force and visualize it using SVG elements; if we want to visualize the outcomes from the d3-force simulation, it is up to us to do so ourselves because that is not what d3-force does. After each iteration of the simulation (called a “tick”) we can use the intermediate calculation results and draw the visualization from these results using SVG – or we do not (d3-force does not care either way)
- we can also let d3-force go through many simulation steps and only use a ‘final’ result to visualize (or not even to visualize) the eventual positions of the elements in the system used for simulation
- d3-force can be used without any other part of d3 and without any degree of visualization.
I have used d3-force to find a way to best distribute visual representations (circles of varying sizes) of data elements – without them overlapping and with a certain repulsion between elements with some characteristics, I could not think of a good algorithm to calculate the positions of these circles; d3-force could run a simulation that produced acceptable final positions for these circles.
Here is the visualized simulation in action:
The essence of the code that produces this behavior:
The d3 library is loaded into this static HTML document. An array with 40 elements is generated. Each element is assigned randomized properties: properties called x and y – whose values are taken from a random range between 0 and w and h (that represent the width and height of some area) and r – mapped to the radius of the circle – and color. Note that initially these are just data objects –they do not have a relationship to SVG objects, HTML elements or anything to do with visualization. D3 force simulations expect – or create – properties called x and y (and optionally vx and vy for the velocity vector components and possibly fx and fy to specify x and y values that the simulation should not alter).
Function doSVG() in this case makes a mapping of these data elements to SVG objects generated using d3 – so we can see an animated representation of the simulation and its final outcome. And when the d3 force simulation acts on these data elements, it updates the x and y coordinates and we use those to redraw the SVG elements.
Note that we could run the simulation without creating any of the SVG elements. The tick handler could just register the final x and y position after a number of iterations and consider that the only outcome of the simulation.
However, in this case – the blipElements are the SVG circle objects created for the node_data array elements – using the properties of these elements to draw the circle. In the tick handler, the position of each circle is updated (by adjusting the transform attribute’s translate instruction), based on the newly calculated x and y property values.
The simulation is defined with three forces: an x and y force that pull all elements to the center of the SVG area and a collide force that prevents the circles from coming to close. The distance a circle keeps from all other circles is set at its radius plus the nodePadding constant.
sim = d3.forceSimulation(node_data)
.force(“x”, d3.forceX(w / 2))
.force(“y”, d3.forceY(h / 2))
.force(“collide”, d3.forceCollide().radius(d => d.r + nodePadding))
The x-force force pulls all elements to the vertical line at x = 300.The y-force pulls elements to the horizontal line at y = 200. Without the collide (counter) force, all elements would move to ( 300, 200), all overlapping at the center of this little system. However, the collide force prevents collisions (or: overlap) and keeps the elements at a distance. Each circle element does not come closer than its own radius plus the node padding constant, set at 3, to other elements. Since that same logic (defined in the function that is passed to d3.forceCollide().radius()) is applied to all elements, all circles have a buffer zone between them of at least 6.
As soon as the simulation is defined, it starts running. It can be stopped (with sim.stop()). And handlers can be registered with it – such as the tick handler shown here. After each iteration of the simulation – when the new x and y property values have been calculated for all data elements, the function registered as tick handler is called. The new x and y values are available and can be used to do ….. whatever. One option is to update the UI representation of the data elements – but remember that this is completely optional.
In addition to the out of the box forces shipped in d3-force, developers can include custom forces in a simulation. When the simulation runs, the custom force function is invoked to adjust the positions and velocities of all elements in the system as it sees fit. The effects of the custom force are combined with the effects of other the forces at play.
Here you see the effect of a custom force that is has been defined to expel elements from a central area depicted with the red rectangle:
Just to be sure: d3-force is not aware of the red rectangle. It is drawn purely for your convenience. However, the custom force added to the simulation corrects any element that enters the area.
We can define custom forces with any logic we fancy – and make for example the circle elements gather around various interesting shapes.
My own earlier article – Introduction of SVG and D3 for the creation of a browser based simple game https://technology.amis.nl/frontend/starting-my-own-game-studio-on-the-ease-and-the-power-of-svg/
Sources for the simulations discussed in this article: https://github.com/lucasjellema/code-cafe-intro-to-svg
A tutorial to using
d3-force from someone who just learned how to use it by Ben Tanen (2021)- https://observablehq.com/@ben-tanen/a-tutorial-to-using-d3-force-from-someone-who-just-learned-ho
Intro To Forced Layouts – jkeohan (2018) – https://observablehq.com/@jkeohan/intro-to-forced-layouts
Article on D3 circular packing (grouping of shapes): different x,y force for each group we discern – https://www.d3-graph-gallery.com/graph/circularpacking_group.html
D3 docs on Force – https://github.com/d3/d3-force
Demo of using d3-force to simulate the solar system: https://bl.ocks.org/vasturiano/54dd054d22be863da5afe2db02e033e2