Tuesday, September 14, 2010

Create a chart using .NET 4 and ASP.NET MVC





This is a "pattern" that I have come up with to create a chart on-the-fly using .NET 4 and MVC. I happen to be using MVC2 but I don't believe that there are any v2 features that I'm using so this will work equally well in MVC 1.0. As you'll notice it can also easily be adapted to be used in a web form web app as well.
At the end of 2008 Scott Guthrie announced ASP.NET Charting which you could download and add to your project. Microsoft had bought Dundas Charting and was now giving it away for free. In .NET 4 they have bundled that charting library so you don't need a separate download and all you have to do is include the System.Web.UI.DataVisualization.Charting namespace, however, I'm getting ahead of myself.
Here are the components to the on-the-fly chart generation in an ASP.NET MVC web application.
The Controller
public ActionResult Chart(int id)
{
    ChartGen cg = new ChartGen();
    MemoryStream ms = cg.GenerateChart(id);

    return File(ms.ToArray(), "image/png", "mychart.png");
}

It really is that simple. All the heavy lifting takes place in my ChartGen class. What is happening here is that the GenerateChart() function is getting a memory stream and that is being passed back as a file action result of type image/png. The chart that is being generated is identified by the first parameter (id) passed in to the ShowChart() function. In this example it's a dummy placeholder value but would allow you to pull the data for the chart from a database based on that index.
The View
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <p>
        <img src="/Home/Chart/1" alt="This is a sample chart" />
    </p>
</asp:Content>

In its most simplistic form the view has a reference to the controller as the source attribute of an image tag. That's really all there is to it.
The Chart Generator
public Chart chart { get; set; }
public MemoryStream GenerateChart(int symbolId)
{
    List<Price> priceList = GetPrices().ToList();

    chart = new Chart();
    chart.Customize += new EventHandler(chart_Customize);
    chart.BorderSkin.SkinStyle = BorderSkinStyle.Emboss;
    chart.BackColor = ColorTranslator.FromHtml("#D3DFF0");
    chart.BorderlineDashStyle = ChartDashStyle.Solid;
    chart.Palette = ChartColorPalette.BrightPastel;
    chart.BackSecondaryColor = Color.White;
    chart.BackGradientStyle = GradientStyle.TopBottom;
    chart.BorderlineWidth = 2;
    chart.BorderlineColor = Color.FromArgb(26, 59, 105);
    chart.Width = Unit.Pixel(500);
    chart.Height = Unit.Pixel(300);

    Series series1 = new Series("Series1");
    series1.ChartArea = "ca1";
    series1.ChartType = SeriesChartType.Candlestick;
    series1.Font = new Font("Verdana", 8.25f, FontStyle.Regular);
    series1.BorderColor = Color.FromArgb(180, 26, 59, 105);

    foreach (Price dayBar in priceList)
    {
        bool upDay = dayBar.Open < dayBar.Close;
        series1.Points.Add(new DataPoint
        {
            BackSecondaryColor = upDay ?
                    Color.LimeGreen : Color.Red,
            BorderColor = Color.Black,
            Color = upDay ? Color.LimeGreen : Color.Red,
            AxisLabel = dayBar.Date.ToString("dd-MMM-yy"),
            YValues = new double[] { (double)dayBar.High,
                (double)dayBar.Low, (double)dayBar.Open,
                (double)dayBar.Close }
        });
    }

    chart.Series.Add(series1);

    ChartArea ca1 = new ChartArea("ca1");
    ca1.BackColor = Color.FromArgb(64, 165, 191, 228);
    ca1.BorderColor = Color.FromArgb(64, 64, 64, 64);
    ca1.BorderDashStyle = ChartDashStyle.Solid;
    ca1.BackSecondaryColor = Color.White;
    ca1.ShadowColor = Color.Transparent;
    ca1.BackGradientStyle = GradientStyle.TopBottom;

    ca1.Area3DStyle.Rotation = 10;
    ca1.Area3DStyle.Perspective = 10;
    ca1.Area3DStyle.Inclination = 15;
    ca1.Area3DStyle.IsRightAngleAxes = false;
    ca1.Area3DStyle.WallWidth = 0;
    ca1.Area3DStyle.IsClustered = false;

    ca1.AxisY.LineColor = Color.FromArgb(64, 64, 64, 64);
    ca1.AxisX.MajorGrid.LineColor = Color.Transparent;
    ca1.AxisY.MajorGrid.LineColor = Color.FromArgb(64, 64, 64, 255);
    ca1.AxisY.MajorGrid.LineDashStyle = ChartDashStyle.Dash;

    double max = (double)priceList.Select(a => a.High).Max();
    double min = (double)priceList.Select(a => a.Low).Min();
    double rangeAdjust = (max - min) * 0.03;
    max += rangeAdjust;
    min -= rangeAdjust;
    ca1.AxisY.Minimum = min;
    ca1.AxisY.Maximum = max;

    chart.ChartAreas.Add(ca1);

    MemoryStream memoryStream = new MemoryStream();
    chart.SaveImage(memoryStream, ChartImageFormat.Png);
    memoryStream.Seek(0, SeekOrigin.Begin);

    return memoryStream;
}

void chart_Customize(object sender, EventArgs e)
{
    CustomLabelsCollection yAxisLabels = chart.ChartAreas["ca1"].AxisY.CustomLabels;

    for (int labelIndex = 0; labelIndex < yAxisLabels.Count; labelIndex++)
    {
        decimal price = Convert.ToDecimal(yAxisLabels[labelIndex].Text);
        // Do your formatting of price here
        yAxisLabels[labelIndex].Text = (price/100).ToString("0.00");
    }
}

Random r = new Random((int)DateTime.Now.Ticks);
IEnumerable<Price> GetPrices()
{
    int open, high, low, close = r.Next(4000, 6000);
    for (int i = -20; i < 1; i++)
    {
        open = r.Next(close - 30, close + 30);
        close = r.Next(open - 70, open + 70);
        high = Math.Max(open, close);
        high = r.Next(high, high + 100);
        low = Math.Min(open, close);
        low = r.Next(low - 100, low);

        yield return new Price
        {
            Date = DateTime.Now.AddDays(i).Date,
            Open = open,
            High = high,
            Low = low,
            Close = close
        };
    }
}

That's a chunk of code to read through but it's not that bad.
We start off by creating a Chart object and attaching an event handler to the Customize property. This event is raised when all the axis and data have been calculated for the chart and just before the chart is rendered. This allows you to change formatting and options on the chart. For example the Chart object will generate values for you on the Y-Axis, when the customize event is raised you can format these values.
There are a bunch of colors and formats you can set for the chart.
The Series object allows you to define a series of data that will be displayed on the chart. In this example we're displaying stock market data in a candlestick format so we set the appropriate data. Once we've defined the series we add it to the chart object.
The rest of the code addresses mostly formatting. There is some code that sets the maximum and minimum values for the Y-Axis so that they are 3% off the lows and highs.
Finally we generate a memory stream and return this image as a memory stream.
I usually have caching in there as well and will cache the memory stream for a period of time using the id passed in to the function as the key to the cache. That way I can have many chart images in memory cache and pull them out without causing them to be generated each time. I have also excluded all the try catch blocks that I would usually have in there.
Here is the Price class that you'll need in the code above:
public class Price
{
    public DateTime Date { get; set; }
    public double Open { get; set; }
    public double High { get; set; }
    public double Low { get; set; }
    public double Close { get; set; }
}

This is the final result:


4 comments:

  1. Good article. There is a way to bind a dataset to the chart. You may know it but I thought I would post it here since the chart controls documentation seems to be lacking from MSFT.
    Chart1.Series["Price"].Points.DataBind(ds.Tables[0].Rows, "date", "open,high,low,close", string.Empty);
    Chart1.Series["Volume"].Points.DataBind(ds.Tables[0].Rows, "date", "Volume", string.Empty);
    Any idea how to skip weekends so that there is no gap in the chart?

    ReplyDelete
  2. Thanks alot. I modified it and now im using it on my website gserp.se.

    ReplyDelete
  3. Excellent sample for dynamically creating Chart in MVC , It is very helpful - Thank you.

    ReplyDelete
  4. FYI -- you can eliminate a step when returning the chart image by using the 'FileStreamResult' ActionResult (you can just return the MemoryStream directly)

    ReplyDelete