0

I want to visualize Activities and their relations with a network model like this

netwok example

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:

enter image description here

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
  • "*Questions asking us to recommend or find a book, tool, software library, tutorial or other off-site resource are off-topic for Stack Overflow as they tend to attract opinionated answers and spam.*" – Diado Oct 26 '18 at 11:31
  • There is nothing out of the box. So you will have to code it all yourself, unless you find a library. I think I would at least consider a MSChart possibly with annotations as the graphics elements. Not very easy for beginners but with many pros wrt to scaling, saving printing. - But as it stands the question is way too braod. – TaW Oct 26 '18 at 14:29
  • But maybe drawing with GDI+ is also a good option. A node and a connection class should do. Btw: Are you sure the sketch is getting the table data right? Is there a rule that maps E&F together? – TaW Oct 26 '18 at 14:33
  • Well, write down the rules to process the table data __very explictly__! No way to code anything before you get that right. – TaW Oct 26 '18 at 14:52
  • @TaW Thanks for your notice, I edited the table and drawing – Yousef Golnahali Oct 26 '18 at 15:12

1 Answers1

1

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

  • It should know its own name and other text data you want to print
  • It also needs to know the previous nodes and to allow tranversing in both direction..
  • ..a list of next nodes
  • It will also know its virtual position in the layout; this will have to be scaled when drawing to fit the given area..
  • And it should know how to draw itself and the connections to its neighbours.

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:

enter image description here

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 ..:

  • a list of nodes and..
  • a list of start nodes. There really should only be one, though; so you may want to throw an 'start node not unique' exception..
  • a list of methods to analyse the node data.

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:

enter image description here

I have made a few changes:

  • I have changed the 2nd "35.2" to "35.3". You probably meant "25.2" but that brings up more data errors; you should take care of them all! Do check the output pane!!
  • I have changed the scales to n.draw(e.Graphics, 50, 30); in the Paint event
  • And finally I changed the font size to Font("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.

TaW
  • 53,122
  • 8
  • 69
  • 111
  • Does it enter the paint? Did you use the debugger to test? Did you [hook up?](https://stackoverflow.com/questions/33275763/copy-datagridview-values-to-textbox/33276161?s=1|0.3901#33276161) the event? – TaW Oct 31 '18 at 10:57
  • So you didn't hook up the Paint event of the pbox?! – TaW Oct 31 '18 at 18:01
  • I put all of my nodes in program and got this error: Index was out of range at layoutNodeY() line: "nodes.Values.ElementAt(i)[j].VY = 1f * j - c / 2" Do you have any suggestion? Thanks – Yousef Golnahali Nov 01 '18 at 21:00
  • Well this is a case for the debugger. The list at `i` contains less than `j` elements. You may want to edit the question to add the node data.. – TaW Nov 01 '18 at 21:05
  • The Question was edited! – Yousef Golnahali Nov 01 '18 at 22:36
  • I don't understand the new table. Neither the 1st not the second column have ubique values, so they can't hold the node names..?!? – TaW Nov 02 '18 at 15:27
  • combining the first and second columns makes the node name. for first row, the node name is 1.6 and 1.5 is its predecessor. this is a way for define WBS – Yousef Golnahali Nov 02 '18 at 19:44
  • Ah. Well, the data you show are incomplete. 1.5 and 5.3 don't exist. – TaW Nov 02 '18 at 20:31
  • yes, that screenshot is a part of 100 rows of datatable! Actually the datatable is made in a long process. i dont know how to show the whole data – Yousef Golnahali Nov 02 '18 at 20:40
  • Well, without the actual data I can't help. But your friend the debugger should point you to the dictionary element that throws and the i and j values.. The whole code could obviously do with some robust error checking to help find anomalies. And of course I'm not 100% sure that the data are the problem. The code could have errors too ! (It was whipped up rather quickly..) – TaW Nov 02 '18 at 20:43
  • Finally i could export my Data as a code, here you can see https://dotnetfiddle.net/SCNQJf – Yousef Golnahali Nov 03 '18 at 09:53
  • Got it. I'll look into it a little later.. – TaW Nov 03 '18 at 10:03
  • The error is in the data. Do have a look at the output! It says _37.1: prevNodeName '35.1' not found or not unique! 35.2: prevNodeName '35.1' not found or not unique! 36.1: prevNodeName '35.1' not found or not unique!_ Also: Do remove all spaces in the list of prevNodes! - To make all those nodes fit in you hould also adapt a few numbers. See my update! – TaW Nov 04 '18 at 08:53