I want to visualize Activities and their relations with a network model like this
I have the table and want to draw the model. Which method do you recommend for doing this issue?
Edit:
When I add a Node Data(a DataTable contains more than 100 rows with Activities and Predecessors Columns) to this Program and using as Node resources , got the
"Index was out of range. Must be non-negative and less than the size of the collection"
(according to @TaW's answer),
in the layoutNodeY() part
line:
nodes.Values.ElementAt(i)[j].VY = 1f * j - c / 2
NodeChart NC = new NodeChart();
private void Form1_Load(object sender, EventArgs e)
{
for (int i = 0; i < sortedtable.Rows.Count - 1; i++)
{ List<string> pred = sortedtable.Rows[i][2].ToString().Split(',').ToList();
for (int j = 0; j < sortedtable.Rows.Count - 1; j++)
{
foreach (var item in pred)
{
if (item == sortedtable.Rows[j][0].ToString() + "." + sortedtable.Rows[j][1].ToString())
{
NC.theNodes.Add(new NetNode(sortedtable.Rows[i][0].ToString() + "." + sortedtable.Rows[i][1].ToString(), item));
}
}
}
}
}
Part of Datatable's Screenshot:
I recommend putting as much of the complexity as possible into data structures.
I make much use of List<T>
and at one time of a Dictionary<float, List<NetNode>>
Note that this post is a good deal longer than SO answers usually are; I hope it is instructive..
Let's start with a node class
These nodes can then be collected and managed in a second class that can analyse them to fill in the lists and the positions.
Here is the result, using your data plus one extra node:
Now let's have a closer lok at the code.
The node class first:
class NetNode
{
public string Text { get; set; }
public List<NetNode> prevNodes { get; set; }
public List<NetNode> nextNodes { get; set; }
public float VX { get; set; }
public float VY { get; set; }
public string prevNodeNames;
public NetNode(string text, string prevNodeNames)
{
this.prevNodeNames = prevNodeNames;
prevNodes = new List<NetNode>();
nextNodes = new List<NetNode>();
Text = text;
VX = -1;
VY = -1;
}
...
}
As you can see it make use of List<T>
to hold lists of itself. Its constructor takes a string
that is expected to contain a list of node names; it will be parsed later by the NodeChart
object, because for this we need the full set of nodes.
The drawing code is simple and only meant as a proof of concept. For nicer curves you can easily improve on it using DrawCurves
with either a few extra points or construct the needed bezier control points.
The arrowhead is also a cheap one; unfortunately the built-in endcap is not very good. To improve you would create a custom one, maybe with a graphicspath..
Here we go:
public void draw(Graphics g, float scale, float size)
{
RectangleF r = new RectangleF(VX * scale, VY * scale, size, size);
g.FillEllipse(Brushes.Beige, r);
g.DrawEllipse(Pens.Black, r);
using (StringFormat fmt = new StringFormat()
{ Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center})
using (Font f = new Font("Consolas", 20f))
g.DrawString(Text, f, Brushes.Blue, r, fmt);
foreach(var nn in nextNodes)
{
using (Pen pen = new Pen(Color.Green, 1f)
{ EndCap = System.Drawing.Drawing2D.LineCap.ArrowAnchor })
g.DrawLine(pen, getConnector(this, scale, false, size),
getConnector(nn, scale, true, size));
}
}
PointF getConnector(NetNode n, float scale, bool left, float size)
{
RectangleF r = new RectangleF(n.VX * scale, n.VY * scale, size, size);
float x = left ? r.Left : r.Right;
float y = r.Top + r.Height / 2;
return new PointF(x, y);
}
You will want to expand the node class to include more text, colors, fonts etc..
The draw method above is one of the longest pieces of code. Let's look at the NodeChart
class now.
It holds ..:
I have left out anything related to fitting the graphics into a given area as well as any error checking..
class NodeChart
{
public List<NetNode> theNodes { get; set; }
public List<NetNode> startNodes { get; set; }
public NodeChart()
{
theNodes = new List<NetNode>();
startNodes = new List<NetNode>();
}
..
}
The first method parses the strings with the names of the previous nodes:
public void fillPrevNodes()
{
foreach (var n in theNodes)
{
var pn = n.prevNodeNames.Split(',');
foreach (var p in pn)
{
var hit = theNodes.Where(x => x.Text == p);
if (hit.Count() == 1) n.prevNodes.Add(hit.First());
else if (hit.Count() == 0) startNodes.Add(n);
else Console.WriteLine(n.Text + ": prevNodeName '" + p +
"' not found or not unique!" );
}
}
}
The next method fills in the nextNodes
lists:
public void fillNextNodes()
{
foreach (var n in theNodes)
{
foreach (var pn in n.prevNodes) pn.nextNodes.Add(n);
}
}
Now we have the data and need to lay out the nodes. The horizontal layout is simple but, as usual with branched data, recursion is needed:
public void layoutNodeX()
{
foreach (NetNode n in startNodes) layoutNodeX(n, n.VX + 1);
}
public void layoutNodeX(NetNode n, float vx)
{
n.VX = vx;
foreach (NetNode nn in n.nextNodes) layoutNodeX(nn, vx + 1);
}
The vertical layout is a bit more complicated. It counts the nodes for each x-position and spreads them out equally. A Dictionary
takes on most of the work: First we fill it in, then we loop over it to set the values. Finally we push the nodes up as much as is needed to center them..:
public void layoutNodeY()
{
NetNode n1 = startNodes.First();
n1.VY = 0;
Dictionary<float, List<NetNode>> nodes =
new Dictionary<float, List<NetNode>>();
foreach (var n in theNodes)
{
if (nodes.Keys.Contains(n.VX)) nodes[n.VX].Add(n);
else nodes.Add(n.VX, new List<NetNode>() { n });
}
for (int i = 0; i < nodes.Count; i++)
{
int c = nodes[i].Count;
for (int j = 0; j < c; j++)
{
nodes.Values.ElementAt(i)[j].VY = 1f * j - c / 2;
}
}
float min = theNodes.Select(x => x.VY).Min();
foreach (var n in theNodes) n.VY -= min;
}
To wrap it up here is how I call it from a Form
with a PictureBox
:
NodeChart NC = new NodeChart();
private void Form1_Load(object sender, EventArgs e)
{
NC.theNodes.Add(new NetNode("A",""));
NC.theNodes.Add(new NetNode("B","A"));
NC.theNodes.Add(new NetNode("C","B"));
NC.theNodes.Add(new NetNode("D","B"));
NC.theNodes.Add(new NetNode("T","B"));
NC.theNodes.Add(new NetNode("E","C"));
NC.theNodes.Add(new NetNode("F","D,T"));
NC.theNodes.Add(new NetNode("G","E,F"));
NC.fillPrevNodes();
NC.fillNextNodes();
NC.layoutNodeX();
NC.layoutNodeY();
pictureBox1.Invalidate();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (NC.theNodes.Count <= 0) return;
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
foreach (var n in NC.theNodes) n.draw(e.Graphics, 100, 33);
}
In addition to the things already mentioned you may want to add a y-scaling or 'leading' parameter to spread the nodes vertically to make more room for extra text..
Update:
Here is the result of the data you provided:
I have made a few changes:
n.draw(e.Graphics, 50, 30);
in the Paint
eventFont("Consolas", 10f)
in the NetNode.draw.You must also make sure the pbox is large enough and/or docked/anchored to allow resizing with the form.