Click here to Skip to main content
Click here to Skip to main content
Go to top

Simple Performance Chart

, 25 Sep 2013
Rate this:
Please Sign up or sign in to vote.
A simple Performance Chart/Monitoring Control


Sample image

Introduction

The Simple Performance Chart is a UserControl that is designed and developed to display varying performance data like reads per second on a disk drive, the bandwidth for a server, or free CPU resources, in a visual, clean manner. It can be controlled by a built-in Timer, which makes synchronized display of values possible. The control offers several formatting options like border style, line colors and styles, widths, a background gradient, and so on.

The purpose of this article is not only to provide necessary information on how to use this simple control, but also to show how a few interesting issues have been solved. It's neither intended for covering all drawing techniques, nor to provide a ready solution for a production environment.

Features

  • Absolute and Relative mode with automatic scaling
  • Built-in Timer for display synchronization or Real-time display
  • Customizable display (colors, line style, borders)

Background

I found many chart drawing projects on the web, but most of them were intended for displaying complete data records in countless possible variations. I could not find a good control that could display a "real-time" chart of my server's performance, which I collected with the PerformanceCounter component (or fetched from other sources). So I wrote my own, and learned a few tricks and techniques, which I'd like to share with you now.

Using the code

Set up the Chart Control

Using the control is pretty easy. Simply link the required assembly reference ("SpPerfChart.dll"), compile your project, choose the SpPerfChart control from the Toolbox and drag it into your Form or any Control. You can edit all of the control's properties in the designer's Properties panel.

Provide performance data

Other than the ability to Clear() the chart from any data, there is only one public method available for providing the data: AddValue(decimal). Any time you obtain a value from anywhere (released by an event, obtained from recurring routines, and so on), add it to the PerfChart control using the AddValue(decimal) method. No need to worry about data preparation or display synchronization. Be sure to set up the PerfChart control properly for your scenario.

Points of Interest

Now that we know how to use the control, let's take a look at some interesting issues and how they have been solved.

Drawing Methods

The actual drawing of the graph is one of the easier parts of the project. I decided to work with two Points, representing the current and the previous value position. While iterating through the value collection, the Point instances are being reused every time. I used a "trick" to avoid an additional condition here: Instead of skipping the drawing of the first line inside the loop (initial "previous value" is zero), the first line is drawn outside the control's bounds.

So let's take a look at the essential parts of the drawing method:

/// <summary>
/// Draws the chart (w/o background or grid, but with border) to the 
/// Graphics <paramref name="g"/>
/// </summary>
/// <param name="g">Graphics</param>
private void DrawChart(Graphics g) {
    visibleValues = Math.Min(this.Width / valueSpacing, drawValues.Count);

    if (scaleMode == ScaleMode.Relative)
        currentMaxValue = GetHighestValueForRelativeMode();

    //"trick": initialize the first previous Point outside the bounds
    Point previousPoint = new Point(Width + valueSpacing, Height);
    Point currentPoint = new Point();

    // Only draw average line when possible (visibleValues) 
    //and needed (style setting)
    // [...]

    // Connect all visible values with lines
    for (int i = 0; i < visibleValues; i++) {
        currentPoint.X = previousPoint.X - valueSpacing;
        currentPoint.Y = CalcVerticalPosition(drawValues[i]);

        // Actually draw the line
        g.DrawLine(perfChartStyle.ChartLinePen.Pen, previousPoint, 
                   currentPoint);

        previousPoint = currentPoint;
    }

    // Draw current relative maximum value string
    // [...]

    // Draw Border on top
    ControlPaint.DrawBorder3D(g, 0, 0, Width, Height, b3dstyle);
}

This looks quite straightforward, and it really is! Take a look at the following source code to learn how to draw a background gradient and the grid. I won't waste many words on it.

/// <summary>
/// Draws the background gradient and the grid into Graphics 
/// <paramref name="g"/>
/// </summary>
/// <param name="g">Graphic</param>
private void DrawBackgroundAndGrid(Graphics g) {
    // Draw the background Gradient rectangle
    Rectangle baseRectangle = new Rectangle(0, 0, this.Width, this.Height);
    using (Brush gradientBrush = new LinearGradientBrush(
         baseRectangle, perfChartStyle.BackgroundColorTop, 
         perfChartStyle.BackgroundColorBottom, LinearGradientMode.Vertical))
    {
        g.FillRectangle(gradientBrush, baseRectangle);
    }

    // Draw all visible, vertical gridlines (if wanted)
    if (perfChartStyle.ShowVerticalGridLines) {
        for (int i = Width - gridScrollOffset; i >= 0; i -= GRID_SPACING) {
            g.DrawLine(perfChartStyle.VerticalGridPen.Pen, i, 0, i, Height);
        }
    }

    // Draw all visible, horizontal gridlines (if wanted)
    if (perfChartStyle.ShowHorizontalGridLines) {
        for (int i = 0; i < Height; i += GRID_SPACING) {
            g.DrawLine(perfChartStyle.HorizontalGridPen.Pen, 
                       0, i, Width, i);
        }
    }
}

You probably noticed the gridScrollOffset variable here. It's required because while the horizontal gridlines always have the same positions, the vertical grid is scrolling with the chart (line) itself. The gridScrollOffset value is calculated every time when a new performance value is added.

Quick and flicker-free drawing: Double Buffering

All animated elements (or just regions) must redraw on each change, which can cause ugly flickering, depending on the redraw speed and size of the canvas to be redrawn. (You could limit the region to reduce the flickering effect, but since it's scrolling, this is not an option for this chart control.) Basically I found two different options which helped in this case:

You can enable double buffering for a control using the following (recommended) method:

this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

Optionally, you simply can set the control's DoubleBuffered Property:

this.DoubleBuffered = true;
I tested the options without a clear conclusion - the results were satisfying in both cases. The first method is more specific on how to handle drawing and thus more optimized, which is why I recommend it. You can find more information on double-buffering in the References section.

Relative Value Scaling

An essential issue was the capability to display arbitrary values in a proper relationship, limited to the actual viewing range. Defining a fixed Maximum value (for example, a network bandwidth of 100,000 kbits) would result in unrecognizably flat graph changes when the network reports (as it usually might) a low bandwidth usage of maybe 10, 100 or 1,000 kbits. With automatic relative scaling, the highest visible value is the measure for all display calculations. Because it's dynamic, this value is displayed in the upper-left corner of the chart.

The number of visible values is calculated from the control's Width and the (fixed) horizontal value spacing (in pixels). The highest displayed value results from a simple loop that checks all visible values to find the highest one. Finally, a few simple mathematical "rule of three" functions are performed to calculate the actual pixel positions. This is handled by the CalcVerticalPosition() method:

/// <summary>
/// Calculates the vertical Position of a value in relation the chart size,
/// Scale Mode and, if ScaleMode is Relative, to the current maximum value
/// </summary>
/// <param name="value">performance value</param>
/// <returns>vertical Point position in pixels</returns>
private int CalcVerticalPosition(decimal value) {
    decimal result = Decimal.Zero;

    if (scaleMode == ScaleMode.Absolute)
        result = value * this.Height / 100;
    else if (scaleMode == ScaleMode.Relative)
        result = (currentMaxValue > 0) ? 
            (value * this.Height / currentMaxValue) : 0;

    result = this.Height - result;

    return Convert.ToInt32(Math.Round(result));
}
As you can see, the method is quite simple. The currentMaxValue > 0 condition prevents the method from generating a division by zero. It could have been combined with the ScaleMode comparison in the else if clause, but this more structured scheme looked "safer" for future extensions.

Synchronized Value Display

We don't have to care about the display synchronization as long we can be sure that our "performance provider" (actually a class supplying the values) is providing all values at regular intervals. For example, this is very suitable for displaying a CPU usage chart, where we measure the data once a second, controlled by our custom Timer component.

But sometimes, we can't tell how regular the intervals can be, or how many values we receive each second. The Simple Performance Chart offers three synchronization options to address this problem: Simple, SynchronizedAverage and SynchronizedSum. Let's take a look at typical examples for each option:

TimerMode: Simple

Case example: Web server request duration in a testing environment

We want each single request duration to be visible on the chart, so that we can track down conspicuously long-lasting requests. We prefer the Simple TimerMode instead of the manual method because the values are reported in real-time and there could be dozens of requests in a second. Disabled TimerMode would cause a redraw for each value, slowing down the performance of the whole control. We use the Simple TimerMode to refresh the chart only once a second. On every interval, all collected values (during the interval) are "flushed" and displayed in the chart.

TimerMode: SynchronizedAverage

Case example: Web server download file size statistics

Like in the previous case, the values are reported in real-time. Every starting request triggers the AddValue() method, providing the size of the requested file (in bytes). Because we only need a simple, statistical overview of the file throughput, we enable the SynchronizedAverage TimerMode. On each interval it will report exactly one value: the calculated average of all collected values during the interval time span. Example: The provider reported three file downloads during a second: 100 kB, 200 kB and 900 kB. The chart will display the average of 400 kB.

TimerMode: SynchronizedSum

Case example: Web server simultaneous users load

Let's assume that most conditions can be derived from the previous case. But this time, we are interested in the actual amount of users requesting our web server per second. (The actual method parameter can even be a hard-coded 1.0). The SynchronizedSum TimerMode will add all values provided during the interval period.

The Demo Application offers dynamic value generators where all behaviors can be tested and comprehended.

Visual Styling

Let's now cover the styling possibilities of our Performance Chart Library.

It's quite an easy matter to allow extensive formatting possibilities for each single drawn element. You can modify dozens of properties for each single line (like Color, Width or DashPattern, just to name few). I don't want to cover all the possibilities in this article.

Style Properties Object Located in a Sub-Property

I decided to allow different formatting options per element: The lines (main chart line, optional average-level line, optional grid lines) can have custom Colors, Widths and DashStyles. Besides that, there are a few other settings like the background gradient colors and visibility of gridlines.

It wouldn't be a good approach to put all these possible properties directly into the control class itself, so I created a simple class called PerfChartStyle, containing just the formatting properties. A global instance of this class was created and exposed in a property with the same name (PerfChartStyle).

Now, this was the resulting Designer Property Bar:

Sample image

Designer Property Bar (before)

So what's that? The property was not declared as "read-only". Nevertheless, the forms designer doesn't allow a closer look into the style class. Or rather, it doesn't know about it yet. That is what we need to tell the designer.

First attempt:

[TypeConverterAttribute(typeof(ExpandableObjectConverter))]
public class PerfChartStyle
{
    private ChartPen verticalGridPen;
    private ChartPen horizontalGridPen;
    // [...]

A TypeConverter "...provides a unified way of converting types of values to other types, as well as for accessing standard values and subproperties." (from MSDN). With the TypeConverter attribute we tell the component designer which TypeConverter we'd like to use. The ExpandableObjectConverter is a converter which discloses its public and visible properties to a PropertyGrid. I won't cover the details here; for more information on this topic, continue to the references section.

Now that we set up the right TypeConverter, we're not ready yet: Our changes are only effective while in design mode! The UserControl isn't able to keep the changes inside the child properties. That has something to to with the ComponentModel architecture. Oh well, it's not able yet: Let's move from attributes on the class to the actual PerfChartStyle property of the SPPerfChart control and its attributes:

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Category("Appearance"), Description("Appearance and Style")]
public PerfChartStyle PerfChartStyle {
    get { return perfChartStyle; }
    set { perfChartStyle = value; }
}

The first line does the whole trick. Using DesignerSerializationVisibility.Content, we instruct the designer's code generator to generate code for the actual child properties, not the class itself.

Here is the final result:

Sample image

There we go! Expandable style properties, with persisted changes.

References and further reading

Conclusion

This Performance Chart project is a good example of how to create a basic UserControl with custom drawing. But we are not finished yet: Many ideas for possible improvements are waiting! We can improve the design-time integration, have a closer look at possible threading issues, and integrate a hundred new features. But if this simple UserControl can satisfy our needs, our whole effort to implement this solution from scratch is low, thanks to .NET and its straightforward but powerful drawing capabilities.

Ideas for possible improvements

There are countless possibilities to improve this control. Here are some ideas, if you are planning to extend the SPPerfChart.

  • Allow even more settings in the ChartStyle, like grid size or value spacing
  • Properly display negative values (with zero-line support)
  • Allow multiple overlaying chart lines
  • Allow scrolling from left to right
  • Provide ASP.NET support

...and many more features.

History

  • Version 1.02: (2013-09-25) Removed LCPL License, applied standard CPOL License 
  • Version 1.01: (2007-02-09) Minor article changes; License and Disclaimer added
  • Version 1.0: (2007-02-09) Initial public release (Assembly Version: 1.0.2595.43101)
  • Version 0.1: (2007-01-17) Project started (local version)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

eclipse2k1
Web Developer
Germany Germany
Thomas' (professional) software development career started in 1998, where he made his first experience with Java, with main focus on web/internet development with JSP and later, ColdFusion, until he finally was confronted with the Microsoft .NET technology.

Comments and Discussions

 
QuestionNice! PinmemberIan Chen27-Jun-14 11:10 
SuggestionNice Work! PinmemberUnknown_Andres16-Apr-14 18:52 
Questionhow to change size of grid rectangles? PinmemberAli.etedal29-Oct-13 2:09 
QuestionVersion for Visual C# 2010 PinmemberNico Encarnacion15-Jan-13 1:06 
QuestionperfChart with PerformanceCounter to display the CPU usage! PinmembermasterLeon9-Dec-12 21:23 
GeneralMy vote of 5 PinmemberAmmar J H24-Apr-12 6:21 
QuestionNice... Pinmembercipiripper10-Apr-12 1:02 
Questioncpu utilization Pinmemberareejrafiq24-Oct-11 11:22 
Questionadding to ASP.NET web page Pinmemberfaroukg26-Aug-11 20:19 
GeneralExcellent Thomas. Thank you! PinmemberGreg Hazzard30-Apr-11 4:45 
GeneralMy vote of 5 Pinmemberhjgode9-Mar-11 21:26 
Great any easy to use (except for the styles stuff)
GeneralPorted the lib to Compact Framework Pinmemberhjgode9-Mar-11 21:26 
GeneralModification with two curves PinmemberDenny_W27-May-10 9:00 
GeneralRe: Modification with two curves and scroll function PinmemberDenny_W12-Jun-10 22:00 
Questionwhere can i find the SpPerfChart.dll file you have mentiond? PinmemberMember 41513935-Dec-09 19:09 
GeneralAdded new chart type: stacked bar Pinmemberschadwick10-Oct-09 13:42 
GeneralRe: Added new chart type: stacked bar Pinmembereclipse2k111-Oct-09 10:55 
GeneralWhere can I find this controller's file "SpPerfChart.dll" PinmemberMember 463554921-Jun-09 7:38 
Generallinking the .dll Pinmemberbjorn366716-May-09 20:40 
AnswerRe: linking the .dll Pinmembereclipse2k117-May-09 11:17 
QuestionOne question... Pinmembermlyons19-Mar-09 17:36 
AnswerRe: One question... Pinmembereclipse2k120-Mar-09 12:30 
GeneralGood PinmemberPrasadSRCSE13-Mar-09 19:35 
QuestionGreat Job. License? Pinmemberrzaf5-Mar-09 6:39 
AnswerRe: Great Job. License? Pinmembereclipse2k15-Mar-09 10:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140916.1 | Last Updated 25 Sep 2013
Article Copyright 2007 by eclipse2k1
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid