Introduction
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.
Background
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.
GraphItem
This abstract class represents an ordered pair of X (horizontal) and Y (vertical) values.
GraphItemXY<TH,TV>
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
A 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.
GraphDim<T>
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 GraphDimInt
, GraphDimFloat
, GraphDimLong
and GraphDimDateTime
. If you want to declare your own dimensions, check out these classes.
GraphDatastream
A 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
A 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.
GraphDatastreamGenerator<TH,TV,TI>
This is a generic interface that, if implemented, creates type-safe GraphDataStreams
. See the examples below for details
GraphDatasetXY<TH,TV>
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
A 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 GraphBox
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
SineFunction
.
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 );
GraphDatasetXY<float,float> gds
= new GraphDatasetXY < float,float >("Angle", "Sine",gstrm);
return gds;
}
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
GraphBox
, the items are iterated to determine the lower and upper bounds of the data. These limits are used by default (see
AutoScaleDimensions()
in
GraphHelp.cs). However, you can call
MeasureDimensions()
and
RescaleDimensions()
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 IComparer
interface.
History
December 15, 2006. First version of code and demonstration program.