Click here to Skip to main content
15,881,715 members
Articles / Multimedia / GDI+

A flexible charting library for .NET

Rate me:
Please Sign up or sign in to vote.
4.70/5 (1,112 votes)
6 Jun 200730 min read 8.9M   180.2K   2.1K  
Looking for a way to draw 2D line graphs with C#? Here's yet another charting class library with a high degree of configurability, that is also easy to use.
//============================================================================
//ZedGraph Class Library - A Flexible Line Graph/Bar Graph Library in C#
//Copyright (C) 2004  John Champion
//
//This library is free software; you can redistribute it and/or
//modify it under the terms of the GNU Lesser General Public
//License as published by the Free Software Foundation; either
//version 2.1 of the License, or (at your option) any later version.
//
//This library is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//Lesser General Public License for more details.
//
//You should have received a copy of the GNU Lesser General Public
//License along with this library; if not, write to the Free Software
//Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//=============================================================================

using System;
using System.Drawing;
using System.Collections;

namespace ZedGraph
{
	/// <summary>
	/// A collection class containing a list of <see cref="CurveItem"/> objects
	/// that define the set of curves to be displayed on the graph.
	/// </summary>
	/// 
	/// <author> John Champion
	/// modified by Jerry Vos</author>
	/// <version> $Revision: 3.32 $ $Date: 2006/03/05 07:28:16 $ </version>
	[Serializable]
	public class CurveList : CollectionPlus, ICloneable
	{
	#region Properties
		// internal temporary value that keeps
		// the max number of points for any curve
		// associated with this curveList
		private int	maxPts;

		/// <summary>
		/// Read only value for the maximum number of points in any of the curves
		/// in the list.
		/// </summary>
		public int MaxPts
		{
			get { return maxPts; }
		}

		/// <summary>
		/// Read only property that returns the number of curves in the list that are of
		/// type <see cref="Bar"/>.
		/// </summary>
		public int NumBars
		{
			get
			{
				int count = 0;
				foreach ( CurveItem curve in this )
				{
					if ( curve.IsBar )
						count++;
				}

				return count;
			}
		}

		/// <summary>
		/// Read only property that returns the number of pie slices in the list (class type is
		/// <see cref="PieItem"/> ).
		/// </summary>
		public int NumPies
		{
			get
			{
				int count = 0;
				foreach ( CurveItem curve in this )
				{
					if ( curve.IsPie )
						count++;
				}

				return count;
			}
		}

		/// <summary>
		/// Read only property that determines if all items in the <see cref="CurveList"/> are
		/// Pies.
		/// </summary>
		public bool IsPieOnly
		{
 			get
 			{
				bool hasPie = false;
 				foreach ( CurveItem curve in this )
 				{
 					if ( !curve.IsPie )
 						return false;
					else
						hasPie = true;
 				}
				return hasPie;
 			}
		}

		/// <summary>
		/// Determine if there is any data in any of the <see cref="CurveItem"/>
		/// objects for this graph.  This method does not verify valid data, it
		/// only checks to see if <see cref="CurveItem.NPts"/> > 0.
		/// </summary>
		/// <returns>true if there is any data, false otherwise</returns>
		public bool HasData()
		{
			foreach( CurveItem curve in this )
			{
				if ( curve.Points.Count > 0 )
					return true;
			}
			return false;
		}
	#endregion
	
	#region Constructors
	
		/// <summary>
		/// Default constructor for the collection class
		/// </summary>
		public CurveList()
		{
			maxPts = 1;
		}

		/// <summary>
		/// The Copy Constructor
		/// </summary>
		/// <param name="rhs">The XAxis object from which to copy</param>
		public CurveList( CurveList rhs )
		{
			this.maxPts = rhs.maxPts;

			foreach ( CurveItem item in rhs )
			{
				this.Add( (CurveItem) ((ICloneable)item).Clone() );
			}
		}

		/// <summary>
		/// Implement the <see cref="ICloneable" /> interface in a typesafe manner by just
		/// calling the typed version of <see cref="Clone" />
		/// </summary>
		/// <returns>A deep copy of this object</returns>
		object ICloneable.Clone()
		{
			return this.Clone();
		}

		/// <summary>
		/// Typesafe, deep-copy clone method.
		/// </summary>
		/// <returns>A new, independent copy of this class</returns>
		public CurveList Clone()
		{
			return new CurveList( this );
		}

		
	#endregion
	
	#region List Methods
		/// <summary>
		/// Indexer to access the specified <see cref="CurveItem"/> object by
		/// its ordinal position in the list.
		/// </summary>
		/// <param name="index">The ordinal position (zero-based) of the
		/// <see cref="CurveItem"/> object to be accessed.</param>
		/// <value>A <see cref="CurveItem"/> object reference.</value>
		public CurveItem this[ int index ]  
		{
			get { return( (CurveItem) List[index] ); }
			set { List[index] = value; }
		}

		/// <summary>
		/// Indexer to access the specified <see cref="CurveItem"/> object by
		/// its <see cref="CurveItem.Label"/> string.
		/// </summary>
		/// <param name="label">The string label of the
		/// <see cref="CurveItem"/> object to be accessed.</param>
		/// <value>A <see cref="CurveItem"/> object reference.</value>
		public CurveItem this[ string label ]  
		{
			get
			{
				int index = IndexOf( label );
				if ( index >= 0 )
					return( (CurveItem) List[index]  );
				else
					return null;
			}
		}

		/// <summary>
		/// Add a <see cref="CurveItem"/> object to the collection at the end of the list.
		/// </summary>
		/// <param name="curve">A reference to the <see cref="CurveItem"/> object to
		/// be added</param>
		/// <seealso cref="IList.Add"/>
		public void Add( CurveItem curve )
		{
			List.Add( curve );
		}

		/// <summary>
		/// Remove a <see cref="CurveItem"/> object from the collection based on an object reference.
		/// </summary>
		/// <param name="curve">A reference to the <see cref="CurveItem"/> object that is to be
		/// removed.</param>
		/// <seealso cref="IList.Remove"/>
		public void Remove( CurveItem curve )
		{
			List.Remove( curve );
		}

		/// <summary>
		/// Insert a <see cref="CurveItem"/> object into the collection at the specified
		/// zero-based index location.
		/// </summary>
		/// <param name="index">The zero-based index location for insertion.</param>
		/// <param name="curve">A reference to the <see cref="CurveItem"/> object that is to be
		/// inserted.</param>
		/// <seealso cref="IList.Insert"/>
		public void Insert( int index, CurveItem curve )
		{
			List.Insert( index, curve );
		}

		/// <summary>
		/// Return the zero-based position index of the
		/// <see cref="CurveItem"/> with the specified <see cref="CurveItem.Label"/>.
		/// </summary>
		/// <param name="label">The <see cref="String"/> label that is in the
		/// <see cref="CurveItem.Label"/> attribute of the item to be found.
		/// </param>
		/// <returns>The zero-based index of the specified <see cref="CurveItem"/>,
		/// or -1 if the <see cref="CurveItem"/> is not in the list</returns>
		/// <seealso cref="IList.IndexOf"/>
		/// <seealso cref="IndexOfTag"/>
		public int IndexOf( string label )
		{
			int index = 0;
			foreach ( CurveItem p in this )
			{
				if ( String.Compare( p.Label, label, true ) == 0 )
					return index;
				index++;
			}

			return -1;
		}

		/// <summary>
		/// Return the zero-based position index of the
		/// <see cref="CurveItem"/> with the specified <see cref="CurveItem.Tag"/>.
		/// </summary>
		/// <remarks>In order for this method to work, the <see cref="CurveItem.Tag"/>
		/// property must be of type <see cref="String"/>.</remarks>
		/// <param name="label">The <see cref="String"/> label that is in the
		/// <see cref="CurveItem.Tag"/> attribute of the item to be found.
		/// </param>
		/// <returns>The zero-based index of the specified <see cref="CurveItem"/>,
		/// or -1 if the <see cref="CurveItem"/> is not in the list</returns>
		/// <seealso cref="IList.IndexOf"/>
		/// <seealso cref="IndexOf"/>
		public int IndexOfTag( string label )
		{
			int index = 0;
			foreach ( CurveItem p in this )
			{
				if ( p.Tag is string &&
							String.Compare( (string) p.Tag, label, true ) == 0 )
					return index;
				index++;
			}

			return -1;
		}

		/// <summary>
		/// Sorts the list according to the point values at the specified index and
		/// for the specified axis.
		/// </summary>
		public void Sort( SortType type, int index )
		{
			InnerList.Sort( new CurveItem.Comparer( type, index ) );
		}
		
		/// <summary>
		/// Go through each <see cref="CurveItem"/> object in the collection,
		/// calling the <see cref="CurveItem.GetRange"/> member to 
		/// determine the minimum and maximum values in the
		/// <see cref="CurveItem.Points"/> list of data value pairs.  If the curves include 
		/// a stack bar, handle within the current GetRange method. In the event that no
		/// data are available, a default range of min=0.0 and max=1.0 are returned.
		/// If the Y axis has a valid data range and the Y2 axis not, then the Y2
		/// range will be a duplicate of the Y range.  Vice-versa for the Y2 axis
		/// having valid data when the Y axis does not.
		/// If any <see cref="CurveItem"/> in the list has a missing
		/// <see cref="PointPairList"/>, a new empty one will be generated.
		/// </summary>
		/// <param name="bIgnoreInitial">ignoreInitial is a boolean value that
		/// affects the data range that is considered for the automatic scale
		/// ranging (see <see cref="GraphPane.IsIgnoreInitial"/>).  If true, then initial
		/// data points where the Y value is zero are not included when
		/// automatically determining the scale <see cref="Axis.Min"/>,
		/// <see cref="Axis.Max"/>, and <see cref="Axis.Step"/> size.  All data after
		/// the first non-zero Y value are included.
		/// </param>
		/// <param name="isBoundedRanges">
		/// Determines if the auto-scaled axis ranges will subset the
		/// data points based on any manually set scale range values.
		/// </param>
		/// <param name="pane">
		/// A reference to the <see cref="GraphPane"/> object that is the parent or
		/// owner of this object.
		/// </param>
		/// <seealso cref="GraphPane.IsBoundedRanges"/>
		public void GetRange( bool bIgnoreInitial, bool isBoundedRanges, GraphPane pane )
		{
			double	tXMinVal,
					tXMaxVal,
					tYMinVal,
					tYMaxVal;

			Scale scale = pane.XAxis.Scale;
			scale.rangeMin = double.MaxValue;
			scale.rangeMax = double.MinValue;
			scale.lBound = ( isBoundedRanges && !pane.XAxis.MinAuto ) ?
				pane.XAxis.Min : double.MinValue;
			scale.uBound = ( isBoundedRanges && !pane.XAxis.MaxAuto ) ?
				pane.XAxis.Max : double.MaxValue;

			
			foreach ( YAxis axis in pane.YAxisList )
			{
				scale = axis.Scale;
				scale.rangeMin = double.MaxValue;
				scale.rangeMax = double.MinValue;
				scale.lBound = ( isBoundedRanges && !scale.MinAuto ) ? scale.Min : double.MinValue;
				scale.uBound = ( isBoundedRanges && !scale.MaxAuto ) ? scale.Max : double.MaxValue;
			}

			foreach ( Y2Axis axis in pane.Y2AxisList )
			{
				scale = axis.Scale;
				scale.rangeMin = double.MaxValue;
				scale.rangeMax = double.MinValue;
				scale.lBound = ( isBoundedRanges && !scale.MinAuto ) ? scale.Min : double.MinValue;
				scale.uBound = ( isBoundedRanges && !scale.MaxAuto ) ? scale.Max : double.MaxValue;
			}

			// initialize the values to outrageous ones to start
			//xMinVal = yMinVal = y2MinVal = tXMinVal = tYMinVal = Double.MaxValue;
			//xMaxVal = yMaxVal = y2MaxVal = tXMaxVal = tYMaxVal = Double.MinValue;
			maxPts = 1;
			
			// The bounds provide a means to subset the data.  For example, if all the axes are set to
			// autoscale, then the full range of data are used.  But, if the XAxis.Min and XAxis.Max values
			// are manually set, then the Y data range will reflect the Y values within the bounds of
			// XAxis.Min and XAxis.Max.
			//double	xLBound = System.Double.MinValue;
			//double	xUBound = System.Double.MaxValue;
			//double	yLBound = System.Double.MinValue;
			//double	yUBound = System.Double.MaxValue;
			//double	y2LBound = System.Double.MinValue;
			//double	y2UBound = System.Double.MaxValue;


			// Loop over each curve in the collection and examine the data ranges
			foreach( CurveItem curve in this )
			{
				// For stacked types, use the GetStackRange() method which accounts for accumulated values
				// rather than simple curve values.
				if ( ( ( curve is BarItem ) && ( pane.BarType == BarType.Stack || pane.BarType == BarType.PercentStack ) ) ||
					( ( curve is LineItem ) && pane.LineType == LineType.Stack ) )
				{
					GetStackRange( pane, curve, out tXMinVal, out tYMinVal,
									out tXMaxVal, out tYMaxVal );
									//xLBound, xUBound,
									//curve.IsY2Axis ? y2LBound : yLBound,
									//curve.IsY2Axis ? y2UBound : yUBound );
				}
				else
				{
					// Call the GetRange() member function for the current
					// curve to get the min and max values
					curve.GetRange( out tXMinVal, out tXMaxVal,
									out tYMinVal, out tYMaxVal, bIgnoreInitial, true, pane );
									//xLBound, xUBound,
									//curve.IsY2Axis ? y2LBound : yLBound,
									//curve.IsY2Axis ? y2UBound : yUBound,
									//pane );
				}
   				
				// isYOrd is true if the Y axis is an ordinal type
				Scale yScale = curve.GetYAxis( pane ).Scale;
				Scale xScale = pane.XAxis.Scale;
				bool isYOrd = yScale.IsAnyOrdinal;
				// isXOrd is true if the X axis is an ordinal type
				bool isXOrd = xScale.IsAnyOrdinal;
   							
				// For ordinal Axes, the data range is just 1 to Npts
				if ( isYOrd )
				{
					tYMinVal = 1.0;
					tYMaxVal = curve.NPts;
				}
				if ( isXOrd )
				{
					tXMinVal = 1.0;
					tXMaxVal = curve.NPts;
				}

				// Bar types always include the Y=0 value
				if ( curve.IsBar )
				{
					if ( pane.BarBase == BarBase.X )
					{
						if ( pane.BarType != BarType.ClusterHiLow )
						{
							if ( tYMinVal > 0 )
								tYMinVal = 0;
							else if ( tYMaxVal < 0 )
								tYMaxVal = 0;
						}
   					
						// for non-ordinal axes, expand the data range slightly for bar charts to
						// account for the fact that the bar clusters have a width
						if ( !isXOrd )
						{
							tXMinVal -= pane.ClusterScaleWidth / 2.0;
							tXMaxVal += pane.ClusterScaleWidth / 2.0;
						}
					}
					else
					{
						if ( pane.BarType != BarType.ClusterHiLow )
						{
							if ( tXMinVal > 0 )
								tXMinVal = 0;
							else if ( tXMaxVal < 0 )
								tXMaxVal = 0;
						}
   						
						// for non-ordinal axes, expand the data range slightly for bar charts to
						// account for the fact that the bar clusters have a width
						if ( !isYOrd )
						{
							tYMinVal -= pane.ClusterScaleWidth / 2.0;
							tYMaxVal += pane.ClusterScaleWidth / 2.0;
						}
					}
				}

				// determine which curve has the maximum number of points
				if ( curve.NPts > maxPts )
					maxPts = curve.NPts;

				// If the min and/or max values from the current curve
				// are the absolute min and/or max, then save the values
				// Also, differentiate between Y and Y2 values
				
				if ( tYMinVal < yScale.rangeMin )
					yScale.rangeMin = tYMinVal;
				if ( tYMaxVal > yScale.rangeMax )
					yScale.rangeMax = tYMaxVal;
					
				
				if ( tXMinVal < xScale.rangeMin )
					xScale.rangeMin = tXMinVal;
				if ( tXMaxVal > xScale.rangeMax )
					xScale.rangeMax = tXMaxVal;
					
				/*	
				if ( curve.IsY2Axis )
				{
					if ( tYMinVal < y2MinVal )
						y2MinVal = tYMinVal;
					if ( tYMaxVal > y2MaxVal )
						y2MaxVal = tYMaxVal;
				}
				else
				{
					if ( tYMinVal < yMinVal )
						yMinVal = tYMinVal;
					if ( tYMaxVal > yMaxVal )
						yMaxVal = tYMaxVal;
				}
   			
				if ( tXMinVal < xMinVal )
					xMinVal = tXMinVal;
				if ( tXMaxVal > xMaxVal )
					xMaxVal = tXMaxVal;
				*/
			}
		
			pane.XAxis.Scale.SetRange( pane, pane.XAxis );
			foreach ( YAxis axis in pane.YAxisList )
				axis.Scale.SetRange( pane, axis );
			foreach ( Y2Axis axis in pane.Y2AxisList )
				axis.Scale.SetRange( pane, axis );

		}
		

		/// <summary>
		/// Calculate the range for stacked bars and lines.
		/// </summary>
		/// <remarks>This method is required for the stacked
		/// types because (for bars), the negative values are a separate stack than the positive
		/// values.  If you just sum up the bars, you will get the sum of the positive plus negative,
		/// which is less than the maximum positive value and greater than the maximum negative value.
		/// </remarks>
		/// <param name="pane">
		/// A reference to the <see cref="GraphPane"/> object that is the parent or
		/// owner of this object.
		/// </param>
		/// <param name="curve">The <see cref="CurveItem"/> for which to calculate the range</param>
		/// <param name="tXMinVal">The minimum X value so far</param>
		/// <param name="tYMinVal">The minimum Y value so far</param>
		/// <param name="tXMaxVal">The maximum X value so far</param>
		/// <param name="tYMaxVal">The maximum Y value so far</param>
		/// <seealso cref="GraphPane.IsBoundedRanges"/>
		private void GetStackRange( GraphPane pane, CurveItem curve, out double tXMinVal,
									out double tYMinVal, out double tXMaxVal, out double tYMaxVal )
		{
			// initialize the values to outrageous ones to start
			tXMinVal = tYMinVal = Double.MaxValue;
			tXMaxVal = tYMaxVal = Double.MinValue;

			ValueHandler valueHandler = new ValueHandler( pane, false );
			bool isXBase = curve.BaseAxis(pane) is XAxis;

			double lowVal, baseVal, hiVal;

			for ( int i=0; i<curve.Points.Count; i++ )
			{
				valueHandler.GetValues( curve, i, out baseVal, out lowVal, out hiVal );
				double x = isXBase ? baseVal : hiVal;
				double y = isXBase ? hiVal : baseVal;

				if ( x < tXMinVal )
					tXMinVal = x;
				if ( x > tXMaxVal )
					tXMaxVal = x;
				if ( y < tYMinVal )
					tYMinVal = y;
				if ( y > tYMaxVal )
					tYMaxVal = y;

				if ( !isXBase )
				{
					if ( lowVal < tXMinVal )
						tXMinVal = lowVal;
					if ( lowVal > tXMaxVal )
						tXMaxVal = lowVal;
				}
				else
				{
					if ( lowVal < tYMinVal )
						tYMinVal = lowVal;
					if ( lowVal > tYMaxVal )
						tYMaxVal = lowVal;
				}
			}
		}

		/// <summary>
		/// Render all the <see cref="CurveItem"/> objects in the list to the
		/// specified <see cref="Graphics"/>
		/// device by calling the <see cref="CurveItem.Draw"/> member function of
		/// each <see cref="CurveItem"/> object.
		/// </summary>
		/// <param name="g">
		/// A graphic device object to be drawn into.  This is normally e.Graphics from the
		/// PaintEventArgs argument to the Paint() method.
		/// </param>
		/// <param name="pane">
		/// A reference to the <see cref="GraphPane"/> object that is the parent or
		/// owner of this object.
		/// </param>
		/// <param name="scaleFactor">
		/// The scaling factor to be used for rendering objects.  This is calculated and
		/// passed down by the parent <see cref="GraphPane"/> object using the
		/// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust
		/// font sizes, etc. according to the actual size of the graph.
		/// </param>
		public void Draw( Graphics g, GraphPane pane, float scaleFactor )
		{
			// Configure the accumulator for stacked bars
			//Bar.ResetBarStack();

			// Count the number of BarItems in the curvelist
			int pos = this.NumBars;
			
			// sorted overlay bars are a special case, since they are sorted independently at each
			// ordinal position.
			if ( pane.BarType == BarType.SortedOverlay )
			{
				// First, create a new curveList with references (not clones) of the curves
				CurveList tempList = new CurveList();
				foreach ( CurveItem curve in this )
					if ( curve.IsBar )
						tempList.Add( (CurveItem) curve );
				
				// Loop through the bars, graphing each ordinal position separately
				for ( int i=0; i<this.maxPts; i++ )
				{
					// At each ordinal position, sort the curves according to the value axis value
					tempList.Sort( pane.BarBase == BarBase.X ? SortType.YValues : SortType.XValues, i );
					// plot the bars for the current ordinal position, in sorted order
					foreach ( BarItem barItem in tempList )
						barItem.Bar.DrawSingleBar( g, pane, barItem,
							((BarItem)barItem).BaseAxis( pane ),
							((BarItem)barItem).ValueAxis( pane ),
							0, i, scaleFactor );
				}
			}

			// Loop for each curve in reverse order to pick up the remaining curves
			// The reverse order is done so that curves that are later in the list are plotted behind
			// curves that are earlier in the list
			for ( int i=this.Count-1; i>=0; i-- )
			{
				CurveItem curve = this[i];
				
				if ( curve.IsBar )
					pos--;
					
				// Render the curve

				//	if it's a sorted overlay bar type, it's already been done above
				if	( !( curve.IsBar && pane.BarType == BarType.SortedOverlay ) ) 
					curve.Draw( g, pane, pos, scaleFactor );
			}
		}
	#endregion

	}
}


By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Engineer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions