C# Plotting Toy

HP laid off a bunch of us August 25. C#/.NET is in demand here in the Boise area, and elsewhere. While I won't have enough C# experience to get a job where I live, I might be able to get a job somewhere else. I thought you might find what I have learned of interest.

I'm not completely done, but I would like to verify that the publish feature of Microsoft Visual Studio C# actually works. You can run the setup program from here. This should install the first significant C# application that I have written, which loads data from CSV files and plots the data. You can download some files of the correct format here, or here.

I solved the flicker problem! I just needed to call this.Invalidate() every time that I took steps that altered, or might alter, the plot.

Here's the source code.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Net;


namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        // The dimensions of the form brought up by this application.
        SizeF formSize;
        int menuBarHeight;
        // The configuration command dialog window (because we need to get to it from 
        // the double click ListBox event handler.
        Form configDlg;
        public Form1()
        {
            InitializeComponent();
            formSize = this.ClientSize;
            this.Size = new System.Drawing.Size((int)(formSize.Width - 50), 
                                                 (int)(formSize.Height - 50));
            // I wish that I could get the ClientSize of the form (that is, minus the 
            // menu bar), but that doesn't seem to be available.
            menuBarHeight = this.menuStrip1.Location.Y + this.menuStrip1.Size.Height;
        }

        // Set up a color palette to use for drawing lines.
        private List colorChoices = new List() 
            {Color.Yellow, Color.Aqua, Color.Red, Color.Blue, Color.Beige,
             Color.Violet, Color.Coral, Color.CornflowerBlue, 
             Color.Cornsilk, Color.Crimson, Color.Cyan, Color.DarkBlue};
        // This is where the numeric data will go.
        private List numbersArray = new List();
        // This is the largest count of numbers in any element of numbersArray
        int numbersArrayMaxCount = 0;
        // This is the largest value that we have to plot across all rows.
        float numbersArrayMax = 0.0F;
        // The legend information (contained in the first column) goes here.
        private List legendStrings = new List();
        // And this is where the column header information goes (the years going 
        // across, for the first sample.
        private ColHdr colHdr = new ColHdr();
        // This contains a list of all the rows in the CSV file to plot.  We'll default this to 
        // every entry when we first load a CSV file.  The user can use the Configure command to 
        // be more selective.
        List listToShow = new List();

        private float RecalculateMaxValue()
        {
            float maxValue = 0.0F;
            for (int i = 0; i < numbersArray.Count; i++)
            {
                NumbersToPlot numbers = numbersArray[i];
                if (listToShow.Contains(i))
                    for (int j = 1; j < numbers.events.Count; j++)
                        maxValue = Math.Max(maxValue, numbers.max);
            }
            return(maxValue);
        }

        private void openWebPageToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Form dlgWebPage = new Form();
            TextBox box = new TextBox();
            box.Location = new Point(0, 0);
            box.Size = new System.Drawing.Size(300, 50);
            box.Text = "http://www.claytoncramer.com/java/bogusdata.csv";
            dlgWebPage.Controls.Add(box);
            // Add a button.
            Button bOk = new System.Windows.Forms.Button();
            bOk.Text = "OK";
            bOk.Location = new Point(0, box.Size.Height);
            bOk.DialogResult = DialogResult.OK;
            dlgWebPage.Controls.Add(bOk);
            Button bCancel = new System.Windows.Forms.Button();
            bCancel.Text = "Cancel";
            bCancel.Location = new Point(bOk.Size.Width + 10, box.Size.Height);
            dlgWebPage.ClientSize = new System.Drawing.Size(box.Size.Width,
                                                    bCancel.Size.Height + bCancel.Location.Y);
            bCancel.DialogResult = DialogResult.Cancel;
            this.AcceptButton = bOk;
            dlgWebPage.Controls.Add(bCancel); 
            if (DialogResult.OK == dlgWebPage.ShowDialog())
            {
                string url = box.Text;
                if (url.Length > 0)
                {
                    // used to build entire input
                    StringBuilder sb = new StringBuilder();
                    // used on each read operation
                    byte[] buf = new byte[8192];
                    try
                    {
                        HttpWebRequest webpage = (HttpWebRequest)WebRequest.Create(url);
                        HttpWebResponse resp = (HttpWebResponse)webpage.GetResponse();
                        Stream resStream = resp.GetResponseStream();
                        string tempString = null;
                        int count = 0;
                        do
                        {
                            // fill the buffer with data
                            count = resStream.Read(buf, 0, buf.Length);
                            // make sure we read some data
                            if (count != 0)
                            {
                                // translate from bytes to ASCII text
                                tempString = Encoding.ASCII.GetString(buf, 0, count);
                                // continue building the string
                                sb.Append(tempString);
                            }
                        }
                        while (count > 0); // any more data to read?
                    }
                    catch ( System.Net.WebException ex)
                    {
                        MessageBox.Show("Couldn't open " + url, ex.ToString(), 
                                        MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                        return;
                    }
                    // Dispose of the old data set, if any
                    if (numbersArray.Count > 0)
                    {
                        numbersArray.Clear();
                        numbersArrayMax = 0.0F;
                        numbersArrayMaxCount = 0;
                        legendStrings.Clear();
                    }
                    string strLine = sb.ToString();
                    // Okay, time to break this into a series of lines, and feed them into the 
                    // Process1stCSVLine and ProcessCSVLine functions, as though this is a local 
                    // file.
                    char[] charArray = new char[] { '\n' };
                    string[] strArray;
                    if (strLine != null)
                    {
                        // Get rid of any \r characters that might be sitting in here.
                        strLine = strLine.Replace("\r", "");
                        strArray = strLine.Split(charArray);
                        strLine = strArray[0];
                        // Okay, feed in the first line.
                        Process1stCSVLine(strLine);
                        // Now process the rest of the lines, until we run out
                        int dataRowNbr = 0;
                        do
                        {
                            strLine = strArray[dataRowNbr];
                            if ((strArray[dataRowNbr + 1] != null) && (strArray[dataRowNbr + 1].Length > 0))
                                ProcessCSVLine(strArray[dataRowNbr + 1], dataRowNbr++);
                        }
                        while (strArray[dataRowNbr + 1] != null && strArray[dataRowNbr + 1].Length > 0);
                    }
                }
                this.Invalidate();
            }
        }

        // Open the data file and populate legend and numbersArray.
        private void openToolStripMenuItem_Click(object sender, EventArgs e)
        {
            OpenFileDialog openFileDialog1 = new OpenFileDialog();
            openFileDialog1.Title = "Data Input File (CSV)";
            openFileDialog1.DefaultExt = "*.csv";
            openFileDialog1.Filter = "CSV files|*.csv";
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                // This is where we specify the CSV file to open.
                Stream myStream = new FileStream(openFileDialog1.FileName, FileMode.Open);
                if (myStream != null)
                {
                    // Dispose of the old data set, if any
                    if (numbersArray.Count > 0)
                    {
                        numbersArray.Clear();
                        numbersArrayMax = 0.0F;
                        numbersArrayMaxCount = 0;
                        legendStrings.Clear();
                    }

                    StreamReader sr = new StreamReader(myStream);
                    string strLine;
                    // Pull in the first line--the column header line.
                    strLine = sr.ReadLine();
                    // Process first line of this stream.
                    Process1stCSVLine(strLine); 
                    // Now process the rest of the lines, until we run out
                    int dataRowNbr = 0;
                    do
                    {
                        strLine = sr.ReadLine();
                        if (strLine != null)
                            ProcessCSVLine(strLine, dataRowNbr++);
                    }
                    while (strLine != null);
                    // All done, close the stream
                    myStream.Close();
                }
                this.Invalidate();
            }
        }

        void Process1stCSVLine (string strLine)
        {
            string[] strArray;
            char[] charArray = new char[] { ',' };
            if (strLine != null)
            {
                strArray = strLine.Split(charArray);
                colHdr.colHdrStrings = new List();
                for (int i = 2; i < strArray.Length; i++)
                    colHdr.colHdrStrings.Add(strArray[i]);
            }
        }

        void ProcessCSVLine (string strLine, int dataRowNbr)
        {
            string[] strArray;
            char[] charArray = new char[] { ',' };
            
            // Now pull in the data that we are going to plot.
            if (strLine != null)
            {
                System.Console.WriteLine("reading " + strLine);
                strArray = strLine.Split(charArray);
                NumbersToPlot numbers = new NumbersToPlot();
                // Add the legend (the first column) to this list.
                legendStrings.Add(strArray[0]);
                listToShow.Add(dataRowNbr);
                numbers.category = strArray[1];
                numbers.events = new List();
                // We need to keep track of the smallest and largest values we see.
                numbers.min = numbers.max = 0;
                int lastNumber;
                for (int i = 2; i < strArray.Length; i++)
                {
                    numbers.events.Add(float.Parse(strArray[i]));
                    lastNumber = numbers.events.Count - 1;
                    numbers.min = Math.Min(numbers.min, numbers.events[lastNumber]);
                    numbers.max = Math.Max(numbers.max, numbers.events[lastNumber]);
                }
                numbersArray.Add(numbers);
                numbersArrayMax = Math.Max(numbersArrayMax, numbers.max);
                numbersArrayMaxCount = Math.Max(numbersArrayMaxCount, numbers.events.Count);
            }
        }
    
        // Configure which lines in the data set to display.
        private void configureToolStripMenuItem_Click(object sender, EventArgs e)
        {
            SizeF sizef;

            // This is where we specify which lines to plot.
            configDlg = new Form();
            sizef = configDlg.ClientSize;
            // Add a ListBox
            ListBox box = new ListBox();
            box.SelectionMode = SelectionMode.MultiExtended;
            box.DoubleClick += new EventHandler(this.listBoxDoubleClick);
             box.Size = new System.Drawing.Size(configDlg.ClientSize.Width, 
                                                configDlg.ClientSize.Height);
           for (int i = 0; i < legendStrings.Count; i++)
            {
                box.Items.Add(legendStrings[i]);
                box.SetSelected(i, true);
            }
            configDlg.Controls.Add(box);
            box.Location = new Point(0, 0);
            // Add a button to the panel.
            Button bOk = new System.Windows.Forms.Button();
            bOk.Text = "OK";
            bOk.Location = new Point(0, box.Size.Height);
            bOk.DialogResult = DialogResult.OK;
            configDlg.Controls.Add(bOk);
            Button bCancel = new System.Windows.Forms.Button();
            bCancel.Text = "Cancel";
            bCancel.Location = new Point(bOk.Size.Width + 10, box.Size.Height);
            bCancel.DialogResult = DialogResult.Cancel;
            this.AcceptButton = bOk;
            configDlg.Controls.Add(bCancel);
            configDlg.ClientSize = new System.Drawing.Size(box.Width, 
                                                           bCancel.Location.Y + bCancel.Height);

            if (configDlg.ShowDialog(this) == DialogResult.OK)
            {
                System.Console.WriteLine("OK");
                // Grab the selection list from the list box.
                ListBox.SelectedIndexCollection selectedItems = box.SelectedIndices;
                // Now go through and convert this into a list of ints.
                listToShow.Clear();
                for (int i = 0; i < selectedItems.Count; i++)
                    listToShow.Add(selectedItems[i]);
                // Recalculate the maximum value that we have to plot.
                numbersArrayMax = RecalculateMaxValue();
                this.Invalidate();
            }
        }

        private void listBoxDoubleClick(Object s, EventArgs e)
        {
            this.configDlg.DialogResult = DialogResult.OK;
            this.configDlg.Close();
        }

        public SizeF CalcLeftMargin(Graphics g, Font legendFont, List legendStrings)
        {
            // Figure out the width of the legends on the left.  These are the 
            // first column of information in the CSV file.  
            SizeF maxLegend = new SizeF();
            int i;
            SizeF legendStrSize;
            for (maxLegend.Width = maxLegend.Height = 0, i = 0; i < numbersArray.Count; i++)
            {
                legendStrSize = g.MeasureString(legendStrings[i], legendFont);
                maxLegend.Width = Math.Max(maxLegend.Width, legendStrSize.Width);
                maxLegend.Height = Math.Max(maxLegend.Height, legendStrSize.Height);
            }
            maxLegend.Width *= 1.10F;
            return (maxLegend);
        }

        public float CalcTopMargin(Graphics g, Font colHdrFont, ColHdr colHdrStrings)
        {
            // Now figure out how much room we need at the top for the column headers.
            // Complicating this is that we need to adjust font size (perhaps) for too many 
            // columns. 
            float maxColHdrWidth = 0.0F;
            float maxColHdrHeight = 0.0F;
            SizeF colHdrStrSize;
            for (int i = 0; i < colHdr.colHdrStrings.Count; i++)
            {
                colHdrStrSize = g.MeasureString(colHdr.colHdrStrings[i], colHdrFont);
                maxColHdrWidth = Math.Max(maxColHdrWidth, colHdrStrSize.Width);
                maxColHdrHeight = Math.Max(maxColHdrHeight, colHdrStrSize.Height);
             }
            return (maxColHdrHeight);
        }

        public float CalcRightMargin(Graphics g, Font colHdrFont, ColHdr colHdr)
        {
            // We have to leave a little room on the right side of the last column because 
            // we are centering the column headers under the center point for the lines.
            // So we need to know how much room that will be for the last column header.
            SizeF colHdrStrSize;
            colHdrStrSize = g.MeasureString(colHdr.colHdrStrings[colHdr.colHdrStrings.Count-1], colHdrFont);
            float rightMargin = (colHdrStrSize.Width / 2.0F) * 1.25F;
            return (rightMargin);
        }

        public void PlotLegends(Graphics g, int menuBarHeight, Font legendsFont, 
                                List legendStrings, float maxLegendsHeight, 
                                List listToShow)
        {
           // Now we know how big it is, we can draw the legend strings down the left side.
            Brush myBrush;
            for (int i = 0, legendLineIndex = 0; i < numbersArray.Count; i++)
            {
                if (listToShow.Contains(i))
                {
                    myBrush = new SolidBrush(colorChoices[legendLineIndex % 10]);
                    g.DrawString(legendStrings[i], legendsFont, myBrush, 0,
                        (maxLegendsHeight * legendLineIndex) + menuBarHeight);
                    legendLineIndex++;
                }
            }
        }

        public void PlotColHdrs(Graphics g, int menuBarHeight, Font colHdrFont, ColHdr colHdr, 
                                float rightMargin, float leftMargin, float xScaling,
                                float plotWidth)
        {
            SizeF colHdrStrSize;

            Brush myBrush = new SolidBrush(Color.White);
            float lastColHdrEndsAt = 0.0F;      // not correct, but the first header must appear
            float colHdrStartsAt = 0.0F;
            for (int i = 0; i < colHdr.colHdrStrings.Count; i++)
            {
                // Get the size of the string so that we can move it over to be centered.
                colHdrStrSize = g.MeasureString(colHdr.colHdrStrings[i], colHdrFont);
                colHdrStartsAt = (i * xScaling) + leftMargin - colHdrStrSize.Width;
                // We don't want to step on the last column header.
                if (colHdrStartsAt > lastColHdrEndsAt + 5.0F)
                {
                    g.DrawString(colHdr.colHdrStrings[i], colHdrFont, myBrush,
                                (i * xScaling) + leftMargin - (colHdrStrSize.Width / 2),
                                menuBarHeight);
                    lastColHdrEndsAt = (i * xScaling) + leftMargin + (colHdrStrSize.Width / 2);
                }
            }
        }

        public void PlotLines(Graphics g, int menuBarHeight, float leftMargin, float topMargin, 
                              float bottomMargin, float rightMargin, float xScaling, ColHdr colHdr,
                              float numbersArrayMax, int divisions, float yScaling, Font font)
        {
            Pen pen = new Pen(Color.White, 2);
            // Draw the left, right, top and bottom lines.
            g.DrawLine(pen, leftMargin, topMargin + menuBarHeight, leftMargin, 
                       bottomMargin + menuBarHeight);
            g.DrawLine(pen, rightMargin, topMargin + menuBarHeight, rightMargin, 
                       bottomMargin + menuBarHeight);
            g.DrawLine(pen, leftMargin, topMargin + menuBarHeight, rightMargin,
                       topMargin + menuBarHeight);
            g.DrawLine(pen, leftMargin, bottomMargin + menuBarHeight, rightMargin, 
                       bottomMargin + menuBarHeight);
            // Draw the vertical grid lines.
            int xCoord = 0;
            for (int i = 0; i < colHdr.colHdrStrings.Count; i++)
            {
                xCoord = (int)((float)i * xScaling + leftMargin);
                g.DrawLine(pen, xCoord, topMargin + menuBarHeight, xCoord, 
                           bottomMargin + menuBarHeight);
            }
            // The brush for drawing the numbers left of the grid lines.
            Brush myBrush = new SolidBrush(Color.White);
            // Draw the horizontal grid lines.
            float perDivision = (yScaling * numbersArrayMax) / divisions;
            float perDivisionNumber = 0.0F;
            for (int i = 0; i < divisions + 1; i++)
            {
                g.DrawLine(pen, leftMargin, bottomMargin - (i * perDivision) + menuBarHeight,
                           rightMargin, bottomMargin - (i * perDivision) + menuBarHeight);
                perDivisionNumber = i * numbersArrayMax / divisions;
                // Figure out how much we have to move the string to the left of the vertical grid 
                // line, and how much we have to raise it up to center on the horizontal grid line.
                SizeF strSize = g.MeasureString(perDivisionNumber.ToString(), font);
                g.DrawString(perDivisionNumber.ToString(), font, myBrush,
                             leftMargin - strSize.Width,
                             (bottomMargin - (i * perDivision)) - strSize.Height / 2 + menuBarHeight);
            }
        }

        public void PlotData(Graphics g, int menuBarHeight, NumbersToPlot numbers, 
                             Pen pen, float leftMargin, 
                             float xScaling, float yScaling,
                             float panelHeight)
        {
            for (int j = 1; j < numbers.events.Count; j++)
            {
                float value = (float)numbers.events[j];
                float prevValue = (float)numbers.events[j - 1];
                Point prevValueCoord = new Point((int)((j - 1) * xScaling + leftMargin),
                                                 (int)(panelHeight - (prevValue * yScaling) +
                                                 menuBarHeight));
                Point curValueCoord = new Point((int)(j * xScaling + leftMargin),
                                                (int)(panelHeight - (value * yScaling) +
                                                menuBarHeight));
                g.DrawLine(pen, prevValueCoord, curValueCoord);
            }
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            Graphics g = e.Graphics;
            if (numbersArray.Count > 0)
            {
                // Clear the panel.
                g.Clear(Color.Black);
                // Create legend font.
                Font legendFont = new System.Drawing.Font("Times Roman", 10.0F,
                    System.Drawing.FontStyle.Regular,
                    System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                // Calculate left margin from legend strings.
                SizeF maxLegendDimensions = CalcLeftMargin(g, legendFont, legendStrings);
                // Create column header font.
                Font colHdrFont = new System.Drawing.Font("Times Roman", 10.0F,
                    System.Drawing.FontStyle.Regular,
                    System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                // Calculate top margin from column headers.
                float topMargin = CalcTopMargin(g, colHdrFont, colHdr) + 20.0F;
                // Calculate right margin from column headers.
                float rightMargin = CalcRightMargin(g, colHdrFont, colHdr);
                PlotLegends(g, menuBarHeight, legendFont, legendStrings, 
                            maxLegendDimensions.Height, listToShow);
                g.PageUnit = GraphicsUnit.Pixel;
                // Calculate the scaling required.
                float yScaling;
                SizeF sizef = g.VisibleClipBounds.Size;
                rightMargin = sizef.Width - rightMargin;
                float leftMargin = maxLegendDimensions.Width;
                // Leave some room at the bottom of the display.  Using 2*menuBarHeight is 
                // kind of cheating; it is actually menuBarHeight + half of the height of the 
                // numbers going down the left side of the grid lines.
                float bottomMargin = sizef.Height - 2 * menuBarHeight;
                // Before we do the y-scaling, we want to round the maximum y-axis value up to 
                // a nice even number.
                numbersArrayMax = (float)RoundUp(numbersArrayMax);
                yScaling = (float)((bottomMargin - topMargin) / numbersArrayMax);
                float xScaling = (float)(rightMargin - leftMargin) / (numbersArrayMaxCount - 1);
                // Write the column header strings.
                PlotColHdrs(g, menuBarHeight, colHdrFont, colHdr, rightMargin, leftMargin, xScaling,
                            rightMargin - leftMargin);
                // Draw the grid lines.
                PlotLines(g, menuBarHeight, leftMargin, topMargin, bottomMargin, rightMargin, xScaling,
                          colHdr, numbersArrayMax, 4, yScaling, colHdrFont);
                for (int i = 0, legendLineIndex = 0; i < numbersArray.Count; i++)
                {
                    if (listToShow.Contains(i))
                    {
                        Pen pen = new Pen(colorChoices[legendLineIndex % 10], 2);
                        PlotData(g, menuBarHeight, numbersArray[i], pen, maxLegendDimensions.Width,
                                 xScaling, yScaling, bottomMargin);
                        legendLineIndex++;
                    }
                }
            }
            else
                g.Clear(Color.Black);
//            panel1.Invalidate();
        }

        public static double RoundUp(double valueToRound)
        {
            return (Math.Floor(valueToRound + 0.5));
        }

        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private void Form1_ResizeEnd(object sender, EventArgs e)
        {
            // See what the new size is, so that we can resize the panel as well.
            formSize = this.ClientSize;
            this.Size = new System.Drawing.Size((int)(formSize.Width - 50),
                                     (int)(formSize.Height - 50));
            menuBarHeight = this.menuStrip1.Location.Y + this.menuStrip1.Size.Height;
            this.Invalidate();
        }
    }
}

public class ColHdr
{
    public List colHdrStrings;
}


public class NumbersToPlot
{
    public string category;
    public List events;
    public float min, max;
    public int maxNumbers;

    public NumbersToPlot()
    {
    }

    public NumbersToPlot(string Category)
    {
        category = Category;
    }
};