Creating Line Charts - Page 1
       by kirupa  |  9 September 2006

We deal with line charts frequently. If you administer web sites, I am sure you have a mechanism of graphically being able to see the relevant traffic data in chart form. If you are into stock trading, charts are used extensively to display a stock's fluctuation over a period of time. There are numerous other examples of charts used everyday, but have you ever thought about how these charts are created?

Before answering that question for you, let me first show you the example chart this tutorial will help you to create in ASP.NET:

Don't let the static image trick you. Refresh this page or open the chart in a new window and keep refreshing that page. Notice that your chart looks different each time the page is loaded!

Since the above example was written entirely in code, in this tutorial I will first devote some time explaining how to approach designing a line chart before delving into how each line of code corresponds to our initial design. Since there are no instructions for you to follow, there will not be any cool screenshots of Visual Studio or anything like that.

Designing this Chart
There are two main features I focused on when designing this chart:

  1. Being able to easily resize and adjust the chart size.

  2. Allowing the chart to automatically adjust to wide ranges and quantities of data.

The rest of the features are what would be common in almost all charts - being able to plot data and and label the vertical and horizontal axes.


Adjusting Chart Size
An important feature I listed above is the ability to easily adjust the chart size. When I say easily adjust the chart size, I am referring not to users being able to drag and resize the chart on the fly, but you as the developer being able to resize it in the code.

This is more tricky than it seems, for resizing your chart area should appropriately resize all of the columns, scale the plotted values appropriately, etc. The following image should provide you a brief overview of the various constraints I placed on the chart:

As you can tell by the above image, your total chart area is only a smaller part of the total area available to it. The reason is because you want some room left over for the various labels, title, etc. Because our .NET code generates an image, you cannot have anything displayed outside of the image boundaries. Which is why the total image width and the total image height are larger than the actual width and height of the chart itself.

In the code, as you will see later on, in order to customize your chart's size, all you need to do is change the appropriate values for the four offsets as well as the total image width and height. The rest is taken care of by our code logic.


Accepting a Wide Range of Values
The chart you design should easily adapt to values outside of an acceptable range. For example, if you had to vertically plot 10,000 and then 10 afterwards, it wouldn't be feasible to have a chart that was at least 10,000 pixels high. Likewise, you wouldn't want your 10 value to be plotted vertically near your 10,000 value.

Your chart range should be both realistic as well as constrained by your chart height and width. To complicate things further, you may have many values that need to be plotted, or you may only have a few values that need to be plotted. Your chart should adapt to that variation in number of data points also, for the width of each column between two plotted values depends both on the number of data points being plotted as well as your chart's width.

To top things off, you have to deal with pixel values. For example, in some cases your column widths would need to be 5.3 pixels to ensure that all data points are plotted with the last data point hugging the right edge of the chart area. With a pixel value, your column widths would only be, using the above example, only 5 pixels wide. That means at the end, there will be some unused space associated with the .3 pixels being ignored. Multiply a loss of .3 pixels by each data point, and you are talking about real pixels leading to unnecessary gaps.


Looking at the Code
Now that you have a brief idea of what to expect with designing a chart, let's go look at the code that takes our design and turns into something usable. The following is the full code used for drawing the chart:

I will not be going through every line of code like I normally do. There are two reasons for that:

  1. The code itself is fairly simple. Some of the issues I mentioned earlier, for example the pixel value rounding, I left unresolved for code simplicity.

  2. The number of pages that this would require would be enormous. Instead, future tutorials will focus in detail on bits and pieces of this code.

With that said, I will provide an overview of what the code does and then cover some of the more interesting aspects in greater detail.

Code Overview
The amount of code written may seem like a lot, but hopefully after this section you will see that it is just many lines doing simple things. All of your chart data is stored as an array of integers.

For example, this is how your data might look like in the array:

int[] points = {10, 20, 15, 30, 5, 23, 19};

So you have seven data points with the range of numbers going from a minimum of 5 and a maximum of 23.

Our program takes this integer array, maximum value, and minimum value and gets it to the chart form in the DrawChart method. Inside the DrawChart method, you specify the details of our chart itself. For example, properties such as how wide/tall the chart will be, where the chart offsets (gaps) are, etc, are specified.

Once you have the constraints of our chart specified, it is time to draw our chart. Drawing a line is essentially having a starting point and an ending point, and having infinitely small dots connecting both the starting and ending points. More realistically, a chart works by drawing a line from the first value to the second value, from the second value to the third value, etc. until your last value is reached.

Beyond this, you have a lot of code that generates the various lines, text labels, etc. We aren't using any GUI-based tools to draw the interface. Everything is done in code, and that can be a bit confusing if you have never designed an interface using only code.

Now that you have a basic understanding of what our code is trying to do, let's take a look at some of the more interesting parts of the code in greater detail.

Setting the Image Properties
The output of our code will be an image, and there are four lines that describe the image setup:

bmp = new Bitmap(400, 300);
gfx = Graphics.FromImage(bmp);
gfx.Clear(Color.White);
gfx.SmoothingMode = SmoothingMode.AntiAlias;

An image in computer-terms can be considered a bitmap where each pixel contains some color value. In the first line, I set the bitmap's dimensions to be 400 pixels wide and 300 pixels tall. I apply the bitmap by using the Graphics method to create an image out of our initial bmp definition.

bmp = new Bitmap(400, 300);
gfx = Graphics.FromImage(bmp);
gfx.Clear(Color.White);
gfx.SmoothingMode = SmoothingMode.AntiAlias;

Now that we assigned our bitmap to our Graphics gfx variable, we won't directly be dealing with our Bitmap object. With the gfx.Clear line, I am essentially clearing the background and setting a default White color.

When you draw shapes and lines, by default they are quite jagged. In order to have them look smoother - antialiased - you will need to set your graphic object's SmoothingMode property to SmoothingMode.AntiAlias.

Getting the Maximum and Minimum Values
The data points plotted are originally stored in an array. In the code above, you will see that a generateRandomValues() method returns an array containing what the method name promises, random values.

There are many ways to determine the maximum values. One, less efficient way, is the method I explained for Flash in the following tutorial. In this code, I take a more efficient approach:

int[] p = generateRandomValues();
 
int[] k = new int[p.Length];
 
Array.Copy(p, k, p.Length);
Array.Sort(k);
 
DrawChart(p, k[k.Length - 1], k[0]);

In this code, what I am doing is storing the array of numbers in the p variable. I then make a copy of the p variable and store it into array k. I will explain why I do that in a few lines.

The Array.sort() method takes an array as its argument and sorts numbers inside it from largest to smallest. Therefore, the array's numbers would be ordered in smallest to largest like:

k = [smallest,...,largest];

So by simply taking the first value, I get the largest value in the array. By taking the last value, I get the smallest value in the array. Because the Array.sort() method modifies the contents of the array itself, I cannot plot array k. If I were to plot array k, my chart would simply increase from the smallest numbers on the left.

That is why I created a copy of my array prior to actually sorting the array's numbers. That way, I independently have both the max and min values while still having a copy of my original array with which to plot the various numbers.

Normalizing the Chart
One of my design goals was to make the chart be capable of plotting a wide range of values. Like I mentioned earlier, you are constrained by your chart's height and width, so you will need to ensure that the highest and lowest values of your chart display with a reasonably accurate scale.

The following is the code that normalizes the chart's height and width:

// Taking care of some bookwork (declaring/initializing variables)
int maxDataPoints = points.Length;
int chartHeight = bmp.Height - bottomOffset;
int chartWidth = bmp.Width - rightOffset;
 
// Adjustable Values
double adjustedMax = maxValue * .10 + maxValue;
double adjustedMin = minValue - .50 * minValue;
double adjustVerticalRatio = (chartHeight-topOffset) / adjustedMax;
double adjustHorizontalRatio = ((chartWidth - leftOffset) / (maxDataPoints - 1));

One good way of looking at this is via an example. Let's say your maximum data point is 1000, and your chart's height is 100 pixels. That means, for every pixel, you have to cover 10 points of data so that you can display your 1000-value data point. That means that a value of 500, will be 50 pixels high. Similarly, a data point that is 100, will only be 10 pixels tall.

What you are trying to do is come up with a good ratio between your chart's height and the number of data values each pixel will cover. The ratio is, as shown above, determined only by your maximum data point, and the ratio is what the adjustVerticalRatio variable stores.

Likewise for the horizontal case, the adjustHorizontalRatio variable stores the distance each data point must be separated by in order to fill up the horizontal space. Ideally, that would be the chart's width divided by the number of data points.

Notice in a lot of the above cases, I take into account any vertical or horizontal offsets that you may have introduced.

Plotting the Lines
To plot the lines, you first determine the starting and ending X and Y positions. Because in order to draw any lines, you need a starting point as well as an ending point. In our case, since all the lines are interconnected, your ending point for one line is the starting point for the next line.

The code for determining the two x and y positions can be found below:

int xPos = Convert.ToInt32(i * adjustHorizontalRatio) + leftOffset;
int xPos2 = Convert.ToInt32((i + 1) * adjustHorizontalRatio) + leftOffset;
 
int yPos = Convert.ToInt32(chartHeight - adjustVerticalRatio * points[i]);
int yPos2 = Convert.ToInt32(chartHeight - adjustVerticalRatio * points[i + 1]);

Some key things to note are how the earlier adjustHorizontalRatio and adjustVerticalRatio values are being used. Notice also that I am converting all of the earlier data into integers, for as you will see in the next line, the x and y positions are stored in Point class that only accepts integer values:

gfx.DrawLine(chartPen, new Point(xPos, yPos), new Point(xPos2, yPos2));

Notice that you use the Point class which takes two integers for its x and y coordinates.

Drawing Border Lines
If you look at the chart, you will see that it contains border lines that clearly separate the chart from the rest of the drawing area. Those border lines are drawn using the same DrawLine method I mentioned earlier.

//Draw Border Lines
Pen borderLine = new Pen(Color.DarkGray, 2);
 
//Left Border
gfx.DrawLine(borderLine, new Point(leftOffset, chartHeight), new Point(leftOffset, topOffset));
 
//Bottom Border
gfx.DrawLine(borderLine, new Point(leftOffset, chartHeight), new Point(chartWidth, chartHeight));
 
//Right Border
gfx.DrawLine(borderLine, new Point(chartWidth, chartHeight), new Point(chartWidth, topOffset));
 
//Top Border
gfx.DrawLine(borderLine, new Point(leftOffset, topOffset), new Point(chartWidth, topOffset));

The DrawLine method takes three arguments in my case. The first argument is a Pen object that defines both the color and thickness of the line. The second and third arguments are our familiar Point values that each take an x and y integer value.

Notice that the arguments passed into Point correspond to the edges as defined earlier in the following image:

Outputting as an Image
I started off the code explanation by describing how you would setup your image. We go full circle now and have reached the point where the close the image and set its output properties.

The code for outputting the final result as an image is as follows:

//Finalizing and Cleaning Up
Response.ContentType = "image/jpeg";
bmp.Save(Response.OutputStream, ImageFormat.Jpeg);
bmp.Dispose();
gfx.Dispose();
Response.End();

Notice I set the content type as "image/jpeg" to ensure the output is set to the JPEG format. An image is created on the fly each time your .aspx page is accessed, so if you were to place your aspx page between img tags in HTML, you will see the image produced by your code.

Conclusion
I hope this article gave you a brief understanding of not only how to approach designing a chart, but also how to draw in ASP.net.

 

1 | 2 | 3 | 4 | 5




SUPPORTERS:

kirupa.com's fast and reliable hosting provided by Media Temple.