3

Currently, I am able to read data from multiple CSV file and plot line graph using windows form application. However, now I need to plot a line graph based on a CSV file's section name (3rd column of csv file).

Modified/New CSV file: (Added the Section Name column)

Values,Sector,Name
5.55,1024,red
5.37,1536,red
5.73,2048,blue
5.62,2560,.blue
5.12,3072,.yellow
...
  1. Based on the Section Name column, my line graph need to be plotted accordingly in a Single line and different sections must be plotted with different colors, including the legends shown at the side of the graph must be shown based on the different section names.
  2. 1 csv file = 1 Series. But there are same section names in a csv file (csv file example shown above, e.g. red for the 1st 20lines). Same section names = same color. If I open 2 or more csv files = 2 Series. Each Series will have different colors due to different section names in the csv file.

I am quite new with programming, and would really appreciate someone could help me by editing from my code.

Thank you.

Mervin
  • 79
  • 2
  • 9
  • You can separate the data by the section column and use the section names as index into the Series collection instead of using `i`. Best use the section name as the Series.Name! I suggest using a data class containing the two numbers and the string and collect them in a `List`. Then create Series for the `distinct` sections. Then loop over them using `chart.Series[data.Section].Points.AddXY(data.N1, data.N2)`. – TaW Jan 14 '17 at 08:05
  • Hi TaW, are you able to add in/edit my code and show me how it is done? Just started learning programming, so I dont really know which part to change – Mervin Jan 14 '17 at 08:07
  • Done. Note the corrections of the 1st version. Also do use your own data names etc.. – TaW Jan 14 '17 at 08:36
  • where shall those color come from? arbitrary colors for each section? – S.Serpooshan Jan 15 '17 at 11:56
  • Can be random colors for each section. Main part is to show that 1csv file - that contains various section names (Some lines have same section names) is shown in 1 Series. But that Series must have legends based on the various colors from the sections. – Mervin Jan 15 '17 at 12:05

2 Answers2

3

You can separate the data by the section column and use the section names as index into the Series collection instead of using i.

Best use the section name as the Series.Name. I suggest using a data class containing the two numbers and the string and collect them in a List<Dataclass>. Then create Series for the distinct sections. Then loop over them..

Here are a few code examples:

Define a class for your data:

public class Data3
{
    public int N1 { get; set;}
    public double N2 { get; set;}
    public string S1 { get; set;}

    public Data3(double n2, int n1, string s1)
    {
        N1 = n1; N2 = n2; S1 = s1;
    }
}

Pick your own names! Optional but always recommended: Add a nice ToString() overload!

Declare a class level varible:

  List<Data3> data = new List<Data3>();

During the read collect the data there:

  data.Add(new Data3(Convert.ToDouble(pieces[1]), Convert.ToInt32(pieces[0]), pieces[2]));

To plot the chart first create the Series:

 var sections= data.Select(x => x.S1).Distinct<string>();
 foreach (string s in sections)
          chart.Series.Add(new Series(s) { ChartType = SeriesChartType.Line });

Then plot the data; the series can be indexed by their Names:

 foreach (var d in data) chart.Series[d.S1].Points.AddXY(d.N1, d.N2);

I left out the nitty gritty of integrating the code into your application; if you run into issues, do show the new code by editing your question!

A few notes:

  • When in doubt always create a class to hold your data

  • When in doubt always choose classes over structures

  • When in doubt always choose List<T> over arrays

  • Always try to break your code down to small chunks with helpful names.

Example: To read all the data in a csv file create a function to do so:

public void AppendCsvToDataList(string file, List<Data3> list)
{
    if (File.Exists(file))
    {
        var lines = File.ReadAllLines(file);
        for (int l = 1; l < lines.Length; l++)
        {
            var pieces = lines[l].Split(',');
            list.Add(new Data3(Convert.ToInt32(pieces[1]),
                               Convert.ToDouble(pieces[0]), pieces[2]));
        }
    }
}
TaW
  • 53,122
  • 8
  • 69
  • 111
  • Hi TaW, thanks for your answer. I've tried adding in your code, but I am not sure where exactly I should insert the code to. As well as the parts I need to remove. – Mervin Jan 14 '17 at 09:53
  • I've only edited the Read class (Shown above). It will be good if you can edit from my codes instead, really confused where I should add the rest of your code to. – Mervin Jan 14 '17 at 10:21
  • I have added a function to read in the data of one csv file. It uses the much simpler File methods but if you for some reason need to use streams you can do so of course. I'm not sure how much of your code is actually needed. Do you get errors? (I don't see your list declaration). Note my function throws away the header, which may or may not be a godd idea.. – TaW Jan 14 '17 at 10:28
  • Setting up the ChartArea is correct, just make sure you do it only once. Do not change the LegendText of the Series, unless you have better values than the section names! Muchl the rest seems unnecessary. – TaW Jan 14 '17 at 10:32
  • But: Do clarify: Do you want one Series per section name or one Series per csv file? Will the files have section names in common?? – TaW Jan 14 '17 at 10:33
  • One Series per csv file. But there are section names in common in each csv file. – Mervin Jan 14 '17 at 10:44
  • So what to do with them?? Still add to different Series? And use the same colors?? – TaW Jan 14 '17 at 11:00
  • I've edited my Read class. May I know whether I need to include anything else in it? I've commented out/deleted the rest of the codes except the ones shown above. – Mervin Jan 14 '17 at 11:02
  • 1 csv file = 1 Series. But there are similar section names (csv file example shown above). Similar section names = same color. If I open 2 or more csv files = 2 Series. Each Series will have different colors due to different section names in the csv file. – Mervin Jan 14 '17 at 11:06
  • But if there are similar section names for BOTH csv files, it can be the same color in both Series. Legends should show the section names . – Mervin Jan 14 '17 at 11:07
  • OK, looks like I didn't really understand the question well enough. Clarify: 'similar' ! - By default each Series gets one LegendItem. To show the color coded sections you need to add additional LegendItems. All in all the question is getting a little broad.. – TaW Jan 14 '17 at 11:10
  • Same** section name i meant. E.g. .text section name for the first 20 lines, etc. – Mervin Jan 14 '17 at 11:16
3

Updated code:

GraphDemo (Form):

    List<Read> rrList = new List<Read>();

    void openToolStripMenuItem_Click(object sender, EventArgs e)
    {
        OpenFileDialog ff = new OpenFileDialog();
        Read rr;

        ff.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); //"C:\\";
        ff.Filter = "csv files (*.csv)|*.csv|All files (*.*)|*.*";
        ff.Multiselect = true;
        ff.FilterIndex = 1;
        ff.RestoreDirectory = true;

        if (ff.ShowDialog() == DialogResult.OK)
        {
            try
            {
                rrList.Clear();
                foreach (String file in ff.FileNames) //if ((myStream = ff.OpenFile()) != null)
                {
                    rr = new Read(file);
                    rrList.Add(rr); 
                }

                //Populate the ComboBoxes
                if (rrList.Count > 0)
                {
                    string[] header = rrList[0].header; //header of first file
                    xBox.DataSource = header; 
                    yBox.DataSource = header.Clone(); //without Clone the 2 comboboxes link together!
                }
                if (yBox.Items.Count > 1) yBox.SelectedIndex = 1; //select second item
            }
            catch (Exception err)
            {
                //Inform the user if we can't read the file
                MessageBox.Show(err.Message);
            }
        }
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Plot.Draw(rrList, xBox, yBox, chart);
    }

class Read:

public class Read
{
    public int nLines { get; private set; }
    public int nColumns { get; private set; }
    public string[] header { get; private set; }
    public float[,] data { get; private set; }
    public string fileName { get; set; }
    public string[] section { get; private set; }

    public Read(string file)
    {
        string[] pieces;

        fileName = Path.GetFileName(file);  
        string[] lines = File.ReadAllLines(file); // read all lines
        if (lines == null || lines.Length < 2) return; //no data in file
        header = lines[0].Split(','); //first line is header
        nLines = lines.Length - 1; //first line is header
        nColumns = header.Length;

        //read the numerical data and section name from the file
        data = new float[nLines, nColumns - 1]; // *** 1 less than nColumns as last col is sectionName
        section = new string[nLines]; // *** 
        for (int i = 0; i < nLines; i++) 
        {
            pieces = lines[i + 1].Split(','); // first line is header
            if (pieces.Length != nColumns) { MessageBox.Show("Invalid data at line " + (i + 2) + " of file " + fileName); return; }
            for (int j = 0; j < nColumns - 1; j++)
            {
                float.TryParse(pieces[j], out data[i, j]); //data[i, j] = float.Parse(pieces[j]);
            }
            section[i] = pieces[nColumns - 1]; //last item is section
        }
    }

}

class Plot:

public class Plot
{
    //public Plot() { } //no constructor required as we use a static class to be called

    public static void Draw(List<Read> rrList, ComboBox xBox, ComboBox yBox, Chart chart) //***
    {
        int indX = xBox.SelectedIndex;
        int indY = yBox.SelectedIndex;

        chart.Series.Clear(); //ensure that the chart is empty
        chart.Legends.Clear();
        Legend myLegend = chart.Legends.Add("myLegend");
        myLegend.Title = "myTitle";

        //define a set of colors to be used for sections
        Color[] colors = new Color[] { Color.Black, Color.Blue, Color.Red, Color.Green, Color.Magenta, Color.DarkCyan, Color.Chocolate, Color.DarkMagenta }; 

        //use a Dictionary to keep iColor of each section
        // key=sectionName, value=iColor (color index in our colors array)
        var sectionColors = new Dictionary<string, int>();

        int i = 0;
        int iColor = -1, maxColor = -1;
        foreach (Read rr in rrList)
        {
            float[,] data = rr.data;
            int nLines = rr.nLines;
            int nColumns = rr.nColumns;
            string[] header = rr.header;

            chart.Series.Add("Series" + i);
            chart.Series[i].ChartType = SeriesChartType.Line;

            //chart.Series[i].LegendText = rr.fileName;
            chart.Series[i].IsVisibleInLegend = false; //hide default item from legend

            chart.ChartAreas[0].AxisX.LabelStyle.Format = "{F2}";
            chart.ChartAreas[0].AxisX.Title = header[indX];
            chart.ChartAreas[0].AxisY.Title = header[indY];

            for (int j = 0; j < nLines; j++)
            {
                int k = chart.Series[i].Points.AddXY(data[j, indX], data[j, indY]);
                string curSection = rr.section[j];
                if (sectionColors.ContainsKey(curSection))
                {
                    iColor = sectionColors[curSection];
                }
                else
                {
                    maxColor++;
                    iColor = maxColor; sectionColors[curSection] = iColor;
                }
                chart.Series[i].Points[k].Color = colors[iColor];
            }

            i++; //series#

        } //end foreach rr

        //fill custom legends based on sections/colors
        foreach (var x in sectionColors)
        {
            string section = x.Key;
            iColor = x.Value;
            myLegend.CustomItems.Add(colors[iColor], section); //new LegendItem()
        }
    }

}
S.Serpooshan
  • 7,608
  • 4
  • 33
  • 61
  • Ok, I will try it later. Thanks for your time! Will tell you how it goes when I've tried. – Mervin Jan 15 '17 at 12:41
  • For the legends, I would need to show all the section names there (based on their colors) or all colors used in the series there. Best case example: 2 csv files (1file contain .text, .data, .rdata section names, another file contain .text, .data, .rsrc section names). So the legends will show 4 colors (.text, .data, .rdata, .rsrc) in total. And there will be 2 Series (2files) – Mervin Jan 16 '17 at 05:48
  • Currently, if my csv file contains other sections, my GUI show this: http://prntscr.com/dw6lbf , it shows different colors (which is correct), but I need it to be shown on the legends as well. – Mervin Jan 16 '17 at 06:01
  • And if i try to open 6 csv files (contains only 1section name, but multiple lines in each csv), I get this: http://prntscr.com/dw6mle , Legends seems to be correctly indicated, but the graph only has 1 color (the first color). And for the csv file that contains other section names, my legends only show the csv file name, instead of the section names. – Mervin Jan 16 '17 at 06:05
  • I am able to plot the graph with `Plot p = new Plot(); p.DoPlot(rrList, xBox, yBox, chart);`. Will wait for your edited code and check again. – Mervin Jan 16 '17 at 07:28
  • 1
    code updated, it shows different colors for each sections. the colors are specified in Plot class as `Color[] colors = new Color[] { Color.Black, Color.Blue, Color.Red, ...`. you have to add more asyou wish for greater number of sections. but i don't know where should filename be printed? – S.Serpooshan Jan 16 '17 at 08:04
  • Under legends, just need to show the section names will do. No need file names already. Thanks. – Mervin Jan 16 '17 at 08:28
  • Yup, it worked, that's what I wanted. If possible, can you add in more of the comments into the codes? As I do not understand some of them (Typically for Plot class). Thanks a lot!! – Mervin Jan 16 '17 at 09:11
  • For my main.cpp (another project in the same solution), it is a command line interface (C/C++). To generate a csv file, I type in the command line: program.exe -sn .text (Csv contain only .text section) -fn text (csv file name) -f c:\windows\notepad (file to analyze). How would I able to generate the graph (saved as image) automatically after just typing the above command line, without using the GUI? – Mervin Jan 16 '17 at 09:33
  • if you feel some one effort is good enough, you can Upvote on an answer (or comment) to give the answerer '+10' reputation. this requires at least +15 reputation as you have now ;D – S.Serpooshan Jan 16 '17 at 10:14
  • you want to get graph from only one file in that command line scenario? – S.Serpooshan Jan 16 '17 at 10:18
  • Ya, based on user's input (e.g. when they type the above command line, 1 graph will be generated and saved as png image, which includes only the .text section). But if its like, program.exe -fn all -f c:\windows\notepad (Exclude the section name), it will create a csv file that contained data and many different section names. But how do I automate the process of the graph after type that particular command? – Mervin Jan 16 '17 at 11:32
  • Hi S.Serp, posted another question regarding command line arguments (http://stackoverflow.com/questions/41788661/c-sharp-how-to-use-flags-in-a-command-line-argument). Do help me if you have the time. Thanks! – Mervin Jan 22 '17 at 11:59