This project creates a graphing control as a subclass of the standard .Net WinForms 2.0
PictureBox control. It integrates easily into the VS2005 Toolbox and supports multiple curves, multiple styles, legends, grids and other features. It can graph functions and relations. Graphs can be automatically scaled or you can keep complete control over scaling.
I did not provide a separate DLL, but you may easily build GraphHelp.cs and GraphBox.cs (included in the demonstration project) together for use in many projects.
All the colors, fonts and display characteristics can be set either from the code directly or in the Visual Studio designer.
The primary difference with this control is the supporting module, GraphHelp.cs.
GraphHelp declares a set of non-generic and generic classes that provide data streams to the control. With these, the control can use any value convertible to numerics as an axis. The demo code shows a
DateTime horizontal axis and a float vertical axis.
Using delegates and generics,
GraphHelp will allow you graph virtually any data type against any other with minimal coding. If you don't want to use generics, you can use the non-generic base classes directly.
Additionally, the default behavior of
GraphBox allows you to save the image to disk. It also supports translation of graph locations to human-readable values using a pop-up "tool tip" window which appears when the mouse button is held down on the graph.
Much of the effort entailed in creating a graph in today's environments comes from translating back and forth from the "native" domain of your data and its types to the rigid demands of a GUI like GDI+. It can be cumbersome to create the bidirectonal (invertible) code necessary to support translation from a point on the graph to a point in data space.
GraphBox and GraphHelp work together to not only display the information but to allow you to create the data containers necessary for safe, reliable management of multiple datasets with a wide variety of data types.
Below is the basic hierarchy of the classes in
GraphHelp. Remember that you can fully utilized GraphBox without instantiating any generic types; they just make things easier if you feel comfortable using them.
Here are the basics. Create a
GraphDataset and associate it with a
GraphBox; the graphing is automatic. Every
GraphDataset has at least one
GraphDatastream to graph;
GraphDatastreams may be empty.
To see how everything fits together, run the program under Debug and breakpoint at
RecreateDataset() in FormGraphDemo.cs. Watch how GraphBox calls
CreateDesignTimeDataset() to built its test datasets.
This abstract class represents an ordered pair of X (horizontal) and Y (vertical) values.
This is the generic version of
GraphItem. Since it contains "raw" data (native times, e.g. DateTime), each relies on its associated
GraphDimension to convert its value back and forth to 'float'.
GraphDimension is an abstract class containing a floating-point range ([minimum..maximum]); it can scale other floating-point values into and out of its range. It also knows how to format the values for printing and graph legends.
This is the generic version of
GraphDimension. It allows for easy declaration of dimensions for new datatypes. In GraphHelp, this class is used as the basis for the classes
GraphDimDateTime. If you want to declare your own dimensions, check out these classes.
GraphDatastream is a collection of
GraphItems representing a single curve or graph.
GraphDatastreams also know how to "bucketize" or consolidate and sort dense datasets for rapid display.
GraphDataset is a collection of (at least one)
GraphDatastreams. This is the class that GraphBox uses to create graphs. In other words, create a dataset and tell a
GraphBox about it-- a graph is produced.
This is a generic interface that, if implemented, creates type-safe
GraphDataStreams. See the examples below for details
This is the generic or type-safe version of
GraphDataset. It has logic to automatically create scaled
GraphDimensions appropriate for the data types you're using. If you create new GraphDimension classes you can inform GraphDatasetXY about them and it will use them when appropriate.
GraphHelper is a silent helper class that is the "bridge" between the
GraphDataset classes and the
GraphBox control. You never create one-- it appears when needed.
Using the code
To use the code, just extract the project from the ZIP file and build it. You must have an up-to-date version of Microsoft's Visual Studio 2005 or the .Net Framework toolset.
The demonstration code is a WinForms application that uses the built-in datasets that GraphBox creates, one of which it uses during "design time" (when you're building an app window).
The core requirement is to give a
GraphDataset containing the data to graph. This can be done one data point at a time by adding GraphItems directly. Alternatively, you can call one of the function evaluators in class GraphDataset or GraphDatasetXY to build the set for you.
In this first example, a dataset is created for "ping" data, where each point represents an ICMP "ping" message happening at a specific date and time; each event takes a measured number of milliseconds to complete.
GraphDatasetXY<DateTime, float> gdb =
new GraphDatasetXY<DateTime, float>( "ping time", "ms round trip" );
foreach (PingNote pn in pt.Notes)
gdset.Add( 0, pn.dtEvent, pn.cmsRoundTrip );
gboxTest.Dataset = gdset;
For another, more complex example, here is a function from GraphBox.cs
. Its job is to create a sine curve represented by a set of points. It accomplishes this using a "delegate" or C# function pointer; in this case, it's called
private GraphDataset DesignTimeDatasetTestSine ( int cPoints )
float fxMin = 0;
float fxMax = (float) (Math.PI * 2);
float fxInc = (fxMax - fxMin) / (float)(cPoints + 1);
GraphDatastream gstrm = GraphDatastream.FromFunction( SineFunction,
fxMin, fxMax, fxInc );
= new GraphDatasetXY < float,float >("Angle", "Sine",gstrm);
private void SineFunction ( float fx, out float fxOut, out float fyOut )
fxOut = fx;
fyOut = (float) Math.Sin( (double) fx );
The key point is that a GraphBox needs a
GraphDataset, which is a container for one or more
GraphDatastreams. The code allows you to use almost any type of data for the streams.
A Note About Scaling
When a simple dataset is given to
, the items are iterated to determine the lower and upper bounds of the data. These limits are used by default (see
). However, you can call
to set your desired values directly. This allows for "zooming".
Points of Interest
This was my second big experience with combining standard C# (abstract classes, delegates, etc.) with the generic capability of C# 2.0. I found that everything generally worked as expected. I was able to work around some of the more challenging aspects of .Net generics, such as the lack of a standardized "numeric interface"; in this case, I relied on the
December 15, 2006. First version of code and demonstration program.<!------------------------------- That's it! --------------------------->