Introduction
This web application produces interactive force-directed graphs. This project started as a fun experiment in combining two of my favourite JavaScript libraries: D3.js and AngularJS. D3.js is a library for drawing and manipulating data with HTML, CSS and SVG and has a vast array of options for many different and beautiful data types.
For this project, I was particularly interested in force-directed graphs, a representation of objects and how they are connected. Force-directed graphs are often used to model road networks, the analysis of protein-protein interaction networks and have also been used to identify disease-trait pairs.
In a force-directed graph, the objects are typically called nodes (also called vertices or points) and the connections between them are called edges (also called lines or arcs). In general, a node can be connected to any other node and have any number of edges. In a force-directed graph, the direction of the connection matters; i.e., Node A → Node B is not the same as Node B → Node A.
Drawing the Graph
D3.js provides tremendous functionality in rendering flexible force-directed graphs. The implementation effectively simulates a series of forces upon the nodes to constrain them according to their connections.
The parameters that can be set for the simulation include:
- Link Distance
- The desired distance between nodes. The simulation calculates a number of other forces that affect how far apart nodes will appear and so the distances are 'springy', but the distance will tend to converge towards this value if the other forces are not excessively powerful. This parameter can be any value greater than zero, and defaults to 20
- Link Strength
- The strength of the force that controls the linkDistance. This affects how rigid the 'spring' forces are. This parameter can be any value between 0 and 1 and defaults to 1
- Friction
- The speed at which node movement slows down. This is not a physical coefficient of friction, rather just a rate at which the nodes tend to come to rest. This parameter can take values between 0 and 1 and defaults to 0.9.
- Charge
- The force that either attracts or repels other nodes. With negative values of charge, nodes will repel eachother and large negative values result in great repulsion. Positive values result in node attraction, but for force-directed graphs then we generally always want a negative value. This parameter can be any value, and defaults to -30.
- Gravity
- A weak geometric constraint that attempts to keep nodes within the centre of the layout. This does not really correspond to a physical 'gravity', it is merely a force that attempts to move nodes towards the center of the graph. At the center of the graph, the gravitational strength is close to zero while further away from the center the gravitational strength increases in proportion to distance. This prevents any loose nodes from escaping away from the layout. This parameter can be any value greater than zero, and defaults to 0.1.
Every single one of these parameters can either be a constant value applied across all nodes, or it can be a function of the nodes or edges. This means that different nodes could have different values of charge, or different edges could have different values of link distance. Some examples of this are included in the presets of the application.
Controlling with AngularJS
AngularJS provides fantastic two-way data binding between the client and the web application. At its most basic, this can be used to watch for changes in an input field, but we also use it for binding more complex data as well.
While D3 controls the rendering of the graph, AngularJS manages the data that is fed into it. The user has control over adding, removing and renaming nodes and connecting nodes together.
Nodes are stored in an array and have the properties ID and Name.
[ { ID: 1, Name: "Node 1" }, { ID: 2, Name: "Node 2"}, { ID: 3, Name: "Node 3" }, { ID: 4, Name: "Node 4" }, { ID: 5, Name: "Node 5" } ]
Edges are stored in the array and have the properties StartNode and EndNode where those parameters refer to the node IDs
[ { StartNode: 1, EndNode: 2 }, { StartNode: 2, EndNode: 3 }, { StartNode: 3, EndNode: 4 }, { StartNode: 4, EndNode: 5 } ]
This would result in a simple chain of Node 1 → Node 2 → Node 3 → Node 4 → Node 5.
These arrays are controlled by Angular and can be manipulated dynamically by the user.
Additionally, all of the parameters for the simulation can be managed by AngularJS as well, allowing users to change the settings and see the effects in real-time.
Code Structure
This is quite a simple Angular application and consists of one controller and two services:
- GraphService
- Responsible for all the logic involved in rendering the graph and all D3.js dependency
- SettingsService
- Response for handling the parameters for the simulation, performing some basic error-checking and storing default values
I have chosen not to implement routing in this application, as I wanted the application to be able to be a standalone application that would run locally. For security reasons, browsers will not load local files and so cannot render the templates for the views without running through a web server. I also did not want to use inline templates, as that would be rather messy with a large template. I could bypass this by putting the templates inside script tags within the main HTML, so that Angular no longer needs to try to load external resources to render a template. At some point, I may refactor to implement this.
Presets
After I'd finished the basic application, I decided to have some fun with it! I wanted to see what I could do with a force-directed graph, so I added a few simple preset arrays of nodes and edges.
For some of the simple ones, I just constructed the arrays manually but for most of the others the arrays are generated programmatically. The output is always just an array of nodes and edges.
Each of the presets also comes with sets of custom graph parameters, which make that particular graph look 'best' but the user can still adjust these afterwards.
I may add new presets once I come up with new and interesting things I can do with force-directed graphs!
Try it out!
And check out the code on on GitHub