C# 从CSV文件中读取字符串并绘制折线图

3

目前,我能够使用Windows窗体应用程序从多个CSV文件中读取数据并绘制折线图。但是,现在我需要基于CSV文件的部分名称(csv文件的第3列)绘制折线图。

修改/新的CSV文件:(添加了“部分名称”列)

Values,Sector,Name
5.55,1024,red
5.37,1536,red
5.73,2048,blue
5.62,2560,.blue
5.12,3072,.yellow
...
  • 根据“Section Name”列,我的折线图需要以“单线”方式绘制,并且不同的部分必须用不同的颜色绘制,包括侧面显示的图例也必须基于不同的部分名称显示。
  • 1个csv文件=1个系列(Series)。但是在csv文件中有相同的部分名称(如上面所示的csv文件示例,例如前20行为红色)。相同的部分名称=相同的颜色。如果我打开两个或更多的csv文件=2个系列(Series)。由于csv文件中的部分名称不同,每个系列将具有不同的颜色。

我对编程还很陌生,希望能有人帮忙修改我的代码。

谢谢。


你可以通过区域列将数据分离,并使用区域名称作为索引进入系列集合,而不是使用“i”。最好使用区域名称作为Series.Name!我建议使用一个包含两个数字和字符串的数据类,并将它们收集到“List<Dataclass>”中。然后为“distinct”区域创建系列。然后使用“chart.Series[data.Section].Points.AddXY(data.N1, data.N2)”循环遍历它们。 - TaW
嗨,TaW,您能否添加/编辑我的代码并向我展示如何完成吗?我刚开始学习编程,所以不太清楚哪个部分需要更改。 - Mervin
完成。请注意第一版的更正。同时,请使用您自己的数据名称等。 - TaW
这些颜色应该从哪里来?每个部分都使用任意颜色吗? - S.Serpooshan
可以为每个部分随机设置颜色。主要目的是展示一个包含各种部分名称(有些行具有相同的部分名称)的1csv文件在1系列中显示。但是,该系列必须根据各个部分的颜色设置图例。 - Mervin
2个回答

3
您可以按照“section”列将数据分开,并使用节名称作为索引进入“Series”集合,而不使用“i”。

最好将部分名称用作“Series.Name”。我建议使用一个包含两个数字和字符串并将它们收集在“List<Dataclass>”中的数据类。然后为不同的部分创建“Series”。 然后循环遍历它们。

以下是一些代码示例:

定义您的数据的“class”:

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;
    }
}

选择您自己的名称!可选但始终建议:添加一个好的ToString()重载!
声明一个类级变量:
  List<Data3> data = new List<Data3>();

在读取数据时,这里有一些需要注意的地方:

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

首先创建Series来绘制图表:

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

然后绘制数据;这些系列可以按其名称进行索引:

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

我省略了将代码集成到您的应用程序中的细节;如果遇到问题,请通过编辑您的问题展示新代码!
几个注意事项:
- 当不确定时,始终创建一个类来保存您的数据。 - 当不确定时,始终选择类而不是结构体。 - 当不确定时,始终选择 List<T> 而不是数组。 - 尽量将您的代码分解为具有有用名称的小块。
例如:要读取 csv 文件中的所有数据,请创建一个函数来执行此操作。
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]));
        }
    }
}

我已经添加了一个函数来读取一个csv文件的数据。它使用更简单的File方法,但如果你因为某些原因需要使用流,当然也可以这样做。我不确定你的代码实际上需要多少。你有错误吗?(我没有看到你的列表声明)。请注意,我的函数会丢弃标题,这可能是一个好主意,也可能不是。 - TaW
那么该怎么办呢?仍然添加到不同的系列中吗?并且使用相同的颜色吗? - TaW
我已经编辑了我的Read类。请问我是否需要在其中包含其他内容?除了上面显示的代码之外,我已经注释掉/删除了其余的代码。 - Mervin
但是,如果两个CSV文件中有相似的部分名称,则可以在两个系列中使用相同的颜色。图例应显示部分名称。 - Mervin
好的,看起来我没有完全理解问题。请澄清:“相似”!- 默认情况下,每个系列都会获得一个LegendItem。要显示彩色编码的部分,您需要添加其他LegendItems。总的来说,这个问题变得有点宽泛了。 - TaW
显示剩余7条评论

3

更新后的代码:

GraphDemo(表单):

    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);
    }

类 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
        }
    }

}

类 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()
        }
    }

}

对于图例,我需要显示所有部分名称(基于它们的颜色)或系列中使用的所有颜色。最佳案例:2个csv文件(1个文件包含.text、.data、.rdata部分名称,另一个文件包含.text、.data、.rsrc部分名称)。因此,图例将总共显示4种颜色(.text、.data、.rdata、.rsrc)。并且将有2个系列(2个文件)。 - Mervin
1
代码已更新,它为每个部分显示不同的颜色。这些颜色在Plot类中指定为Color[] colors = new Color[] { Color.Black, Color.Blue, Color.Red, ...}。您可以根据需要添加更多以获取更多部分的颜色。但我不知道文件名应该打印在哪里? - S.Serpooshan
对于我的main.cpp(同一解决方案中的另一个项目),它是一个命令行接口(C/C++)。为了生成一个csv文件,我在命令行中键入:program.exe -sn .text(Csv只包含.text部分)-fn text(csv文件名)-f c:\windows\notepad(要分析的文件)。如果我想在仅键入上述命令行后自动生成图形(保存为图像),而不使用GUI,该怎么办? - Mervin
如果你觉得某个人的回答(或评论)很好,你可以点赞来给回答者+10的声望值。这需要至少15的声望值,而你现在已经有了;D - S.Serpooshan
你想在命令行场景下从一个文件中获取图表吗? - S.Serpooshan
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接