This Graph Visualization control was developed for a project at Pacific Northwest National Laboratory. The requirements were:
- Create a visualization for a general directed hierarchical graph given nodes and their child relationships
- Create the visualization as an ASP.NET 2.0 user control
- Enable the user control to be used in other projects
- Several different types of nodes
- Allow customization of the graph (size and colors)
- Create the visualization such that it can be modified later to add interactive graph-building
How it works (frontend)
canvas div tags (a node canvas and a graphics canvas), a div for an info popup, and some calls to the
visualization object that look something like this:
So, just a few lines of code:
can create a graph like this:
Almost all of the logic is within the draw() function and its helper functions. The draw() function works
something like this:
addPathways(); // See "Problems" below
drawLines(); // Also draws arrowheads - see "Problems" below
These functions operate on VisNode objects, which themselves manage their node's data, page element and
events that operate on their page element.
How it works (backend)
Everything is wrapped up in one user control. This user control contains the required page elements,
Using the standard data-access method (obtain data from the project's web service), all that is required
is a graph id and the control will do the rest. In other words, the following line of ASP.NET code is
all that is required to draw a graph:
<uc:VisControl ID="Graph" GraphId="1021" runat="server" />
This can even be accomplished by dragging-and-dropping the control into the Visual Studio .NET Design
view for a web page - no coding required.
Using a data source other than the web service is easy. All that is required is changing the constructors for the Graph and Node classes. In fact, since Properties are used to access all of the graph data, as long as the Properties are accessible, the classes can be changed in any way.
The graph's stylesheet is organized to make it simple to change the basic styling of the graph. Customizable visual styles (such as canvas size, colors and borders) are all located at the top of the stylesheet while important styles (such as display mode, overflow handling, floating and z-indexing) are all placed below a comment line that says "Changes made below this line may break the graphing control." Additionally, visual styles are grouped intelligently, so changing something like the border color on the canvas will also change the border color of the nodes, key, and key items. In other words, by changing a couple lines of the stylesheet, a graph like this could be created:
This makes it easy to integrate the Graph Visualization control into many different projects, each with different styles.
Overlapping lines and edges
The original graphing algorithm (the same as above, sans the addPathways() function) had problems with overlapping nodes and edges, since it is possible for a node to be both a parent and a grandparent of another node. For example, in the above graph, if node 1 was also a parent of node 3, where would the path to node 3 be drawn? In the original algorithm, all lines were drawn completely straight, and since lines exist on a layer below the nodes, it was impossible to tell if a line was following the path 1->2->3, or if it was following 1->2 and 1->3. This was even more of a problem because arrowheads weren't drawn at the time the issue was encountered.
To solve this problem, I read several research papers on the subject of visualizing graphs. Many of them covered crossing minimization (which didn't help), but a paper by Tom Germano entitled Graph Drawing had a picture that solved my problem. Essentially, the solution was to insert "waypoint" nodes at every level between a parent and a child. The line would follow these nodes between the parent and child node. These nodes would add an invisible block the size of a node through which a line could pass, avoiding other nodes at the same level.
This simple change turned a graph that looked like this:
It is much easier to interpret the relationships in the second graph since lines don't pass through nodes.
Originally, the graph had no arrowheads. I was asked to add arrowheads to reinforce the left-to-right direction of the graph. After some work trying to figure out the math behind how to position points in a triangle to form an arrowhead, I decided instead to use a Rotation Transformation. I decided to draw an arrowhead pointing straight up, and then used the transformation to rotate it around the base of the arrowhead. I then translated (moved) the arrowhead to the correct point on the line. This graphics technique saved me a lot of time and energy, and produced great-looking results.