File: Common\ChartTypes\PointAndFigureChart.cs
Project: ndp\fx\src\DataVisualization\System.Web.DataVisualization.csproj (System.Web.DataVisualization)
//-------------------------------------------------------------
// <copyright company=’Microsoft Corporation’>
//   Copyright © Microsoft Corporation. All Rights Reserved.
// </copyright>
//-------------------------------------------------------------
// @owner=alexgor, deliant
//=================================================================
//  File:		PointAndFigureChart.cs
//
//  Namespace:	DataVisualization.Charting.ChartTypes
//
//	Classes:	PointAndFigureChart
//
//  Purpose:	Point and Figure chart type do not plot series data 
//              point directly as most of the other chart types. 
//              Instead it uses different calculation to create a 
//              new RangeColumn type series based on its data in 
//              the PrepareData method. All the changes in this 
//              method are reversed back in the UnPrepareData 
//              method. RangeColumn chart type is extended to 
//              display a column of Os or Xs.
//	
//	Point and Figure Charts Overview:
//  ---------------------------------
//	
//	Point and Figure charts differ from traditional price charts in 
//	that they completely disregard the passage of time, and only 
//	display changes in prices. Rather than having price on the y-axis, 
//	and time on the x-axis, Point and Figure charts display price 
//	changes on both axes. This is similar to the Kagi, Renko, and 
//	Three Line Break charts.
//	
//	The Point and Figure chart displays the underlying supply and 
//	demand as reflected in the price values. A column of Xs shows 
//	that demand is exceeding supply, which is known as a rally, 
//	a column of Os shows that supply is exceeding demand, which is 
//	known as a decline, and a series of short columns shows that 
//	supply and demand are relatively equal, which of course, 
//	represents a market equilibrium.
//	
//	The following should be taken into account when working with 
//	this type of chart:
//	
//	- The X values of data points are automatically indexed. For 
//	more information see the topic on Indexing Data Point X Values. 
//	
//	- There is a formula applied to the original data before it gets 
//	plotted. This formula changes the number of points, as well as 
//	their X/Y values. 
//	
//	- Due to the data being recalculated, we do not recommend setting 
//	the minimum, or maximum values for the X axis. This is because it 
//	cannot be determined how many data points will actually be plotted. 
//	However,  if the axis' Maximum, or Minimum is set, then the Maximum, 
//	and Minimum properties will use data point index values. 
//	
//	- Data point anchoring, used for annotations, is not supported 
//	with this type of chart. 
//	
//	Reviewed:   AG - Microsoft 6, 2007
//
//===================================================================
 
#region Used namespaces
 
using System;
using System.Resources;
using System.Reflection;
using System.Collections;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.ComponentModel.Design;
using System.Globalization;
 
#if Microsoft_CONTROL
	using System.Windows.Forms.DataVisualization.Charting;
	using System.Windows.Forms.DataVisualization.Charting.Data;
	using System.Windows.Forms.DataVisualization.Charting.ChartTypes;
	using System.Windows.Forms.DataVisualization.Charting.Utilities;
	using System.Windows.Forms.DataVisualization.Charting.Borders3D;
#else
using System.Web.UI.DataVisualization.Charting;
 
using System.Web.UI.DataVisualization.Charting.ChartTypes;
using System.Web.UI.DataVisualization.Charting.Data;
using System.Web.UI.DataVisualization.Charting.Utilities;
#endif
 
#endregion
 
#if Microsoft_CONTROL
	namespace System.Windows.Forms.DataVisualization.Charting.ChartTypes
#else
namespace System.Web.UI.DataVisualization.Charting.ChartTypes
#endif
{
	/// <summary>
    /// PointAndFigureChart class contains all the code necessary for calculation
    /// and drawing Point and Figure chart.
	/// </summary>
	internal class PointAndFigureChart : RangeColumnChart
	{
		#region Fields
 
		/// <summary>
		/// Indicates that class subscribed fro the customize event.
		/// </summary>
		static private	bool	_customizeSubscribed = false;
 
		#endregion // Fields
 
		#region Methods
 
		/// <summary>
		/// Prepares PointAndFigure chart type for rendering. We hide original series
        /// during rendering and only using the data for calculations. New RangeColumn
        /// type series is added wich displayes the columns of Os or Xs.
        /// All the changes in this method are reversed back in the UnPrepareData method. 
		/// </summary>
		/// <param name="series">Series to be prepared.</param>
		internal static void PrepareData(Series series)
		{
			// Check series chart type
			if(String.Compare( series.ChartTypeName, ChartTypeNames.PointAndFigure, StringComparison.OrdinalIgnoreCase ) != 0 || !series.IsVisible())
			{
				return;
			}
 
			// Get reference to the chart control
			Chart	chart = series.Chart;
			if(chart == null)
			{
                throw (new InvalidOperationException(SR.ExceptionPointAndFigureNullReference));
			}
 
            // PointAndFigure chart may not be combined with any other chart types
            ChartArea area = chart.ChartAreas[series.ChartArea];
            foreach (Series currentSeries in chart.Series)
            {
                if (currentSeries.IsVisible() && currentSeries != series && area == chart.ChartAreas[currentSeries.ChartArea])
                {
                    throw (new InvalidOperationException(SR.ExceptionPointAndFigureCanNotCombine));
                }
            }
 
			// Subscribe for customize event
			if(!_customizeSubscribed)
			{
				_customizeSubscribed = true;
				chart.Customize += new EventHandler(OnCustomize);
			}
 
			// Create a temp series which will hold original series data points
			string tempSeriesName = "POINTANDFIGURE_ORIGINAL_DATA_" + series.Name;
			if (chart.Series.IndexOf(tempSeriesName) != -1)
			{
				return; // the temp series has already been added
			}
			Series seriesOriginalData = new Series(tempSeriesName, series.YValuesPerPoint);
			seriesOriginalData.Enabled = false;
			seriesOriginalData.IsVisibleInLegend = false;
			seriesOriginalData.YValuesPerPoint = series.YValuesPerPoint;
			chart.Series.Add(seriesOriginalData);
			foreach(DataPoint dp in series.Points)
			{
				seriesOriginalData.Points.Add(dp);
			}
			series.Points.Clear();
			if(series.IsCustomPropertySet("TempDesignData"))
			{
				seriesOriginalData["TempDesignData"] = "true";
			}
 
 
			// Remember prev. series parameters
			series["OldXValueIndexed"] = series.IsXValueIndexed.ToString(CultureInfo.InvariantCulture);
			series["OldYValuesPerPoint"] = series.YValuesPerPoint.ToString(CultureInfo.InvariantCulture);
			series.IsXValueIndexed = true;
 
			// Calculate date-time interval for indexed series
			if(series.ChartArea.Length > 0 &&
				series.IsXValueDateTime())
			{
				// Get X axis connected to the series
				Axis		xAxis = area.GetAxis(AxisName.X, series.XAxisType, series.XSubAxisName);
 
				// Change interval for auto-calculated interval only
				if(xAxis.Interval == 0 && xAxis.IntervalType == DateTimeIntervalType.Auto)
				{
					// Check if original data has X values set to date-time values and
					// calculate min/max X values.
					bool	nonZeroXValues = false;
					double	minX = double.MaxValue;
					double	maxX = double.MinValue;
					foreach(DataPoint dp in seriesOriginalData.Points)
					{
						if(!dp.IsEmpty)
						{
							if(dp.XValue != 0.0)
							{
								nonZeroXValues = true;
							}
							if(dp.XValue > maxX)
							{
								maxX = dp.XValue;
							}
							if(dp.XValue < minX)
							{
								minX = dp.XValue;
							}
						}
					}
 
					if(nonZeroXValues)
					{
						// Save flag that axis interval is automatic
						series["OldAutomaticXAxisInterval"] = "true";
 
						// Calculate and set axis date-time interval
						DateTimeIntervalType	intervalType = DateTimeIntervalType.Auto;
						xAxis.interval = xAxis.CalcInterval(minX, maxX, true, out intervalType, series.XValueType);
						xAxis.intervalType = intervalType;
					}
				}
			}
 
			// Calculate PointAndFigure bricks data points values
			FillPointAndFigureData(series, seriesOriginalData);
		}
 
		/// <summary>
		/// Remove any changes done while preparing PointAndFigure chart type for rendering.
		/// </summary>
		/// <param name="series">Series to be un-prepared.</param>
		/// <returns>True if series was removed from collection.</returns>
		internal static bool UnPrepareData(Series series)
		{
			if(series.Name.StartsWith("POINTANDFIGURE_ORIGINAL_DATA_", StringComparison.Ordinal))
			{
				// Get reference to the chart control
				Chart	chart = series.Chart;
				if(chart == null)
				{
                    throw (new InvalidOperationException(SR.ExceptionPointAndFigureNullReference));
				}
 
				// Unsubscribe for customize event
				if(_customizeSubscribed)
				{
					_customizeSubscribed = false;
					chart.Customize -= new EventHandler(OnCustomize);
				}
 
				// Get original PointAndFigure series
				Series	pointAndFigureSeries = chart.Series[series.Name.Substring(29)];
                Series.MovePositionMarkers(pointAndFigureSeries, series);
 
				// Copy data back to original PointAndFigure series
				pointAndFigureSeries.Points.Clear();
				if(!series.IsCustomPropertySet("TempDesignData"))
				{
					foreach(DataPoint dp in series.Points)
					{
						pointAndFigureSeries.Points.Add(dp);
					}
				}
 
				// Restore series properties
                bool xValIndexed;
                bool parseSucceed = bool.TryParse(pointAndFigureSeries["OldXValueIndexed"], out xValIndexed);
 
                pointAndFigureSeries.IsXValueIndexed = parseSucceed && xValIndexed;
 
                int yVals;
                parseSucceed = int.TryParse(pointAndFigureSeries["OldYValuesPerPoint"], NumberStyles.Any, CultureInfo.InvariantCulture, out yVals);
 
                if (parseSucceed)
                {
                    pointAndFigureSeries.YValuesPerPoint = yVals;
                }
 
				pointAndFigureSeries.DeleteCustomProperty("OldXValueIndexed");
				pointAndFigureSeries.DeleteCustomProperty("OldYValuesPerPoint");
				pointAndFigureSeries.DeleteCustomProperty(CustomPropertyName.EmptyPointValue);
 
				series["OldAutomaticXAxisInterval"] = "true";
				if(pointAndFigureSeries.IsCustomPropertySet("OldAutomaticXAxisInterval"))
				{
					pointAndFigureSeries.DeleteCustomProperty("OldAutomaticXAxisInterval");
 
					// Reset automatic interval for X axis
					if(pointAndFigureSeries.ChartArea.Length > 0)
					{
						// Get X axis connected to the series
						ChartArea	area = chart.ChartAreas[pointAndFigureSeries.ChartArea];
						Axis		xAxis = area.GetAxis(AxisName.X, pointAndFigureSeries.XAxisType, pointAndFigureSeries.XSubAxisName);
 
						xAxis.interval = 0.0;
						xAxis.intervalType = DateTimeIntervalType.Auto;
					}
				}
 
				// Remove series from the collection
				chart.Series.Remove(series);
				return true;
			}
 
			return false;
		}
 
		/// <summary>
		/// Gets price range in the point and figure chart.
		/// </summary>
		/// <param name="originalData">Series with original data.</param>
		/// <param name="yValueHighIndex">Index of the Y value to use as High price.</param>
		/// <param name="yValueLowIndex">Index of the Y value to use as Low price.</param>
		/// <param name="minPrice">Returns max price.</param>
		/// <param name="maxPrice">Returns min price.</param>
		private static void GetPriceRange(
			Series originalData, 
			int yValueHighIndex, 
			int yValueLowIndex, 
			out double minPrice,
			out double maxPrice)
		{
			// Calculate percent of the highest and lowest price difference.
			maxPrice = double.MinValue;
			minPrice = double.MaxValue;
			foreach(DataPoint dp in originalData.Points)
			{
				if(!dp.IsEmpty)
				{
					// Check required Y values number
					if(dp.YValues.Length < 2)
					{
						throw(new InvalidOperationException(SR.ExceptionChartTypeRequiresYValues(ChartTypeNames.PointAndFigure, ((int)(2)).ToString(CultureInfo.CurrentCulture))));
					}
 
					if(dp.YValues[yValueHighIndex] > maxPrice)
					{
						maxPrice = dp.YValues[yValueHighIndex];
					}
					else if(dp.YValues[yValueLowIndex] > maxPrice)
					{
						maxPrice = dp.YValues[yValueLowIndex];
					}
 
					if(dp.YValues[yValueHighIndex] < minPrice)
					{
						minPrice = dp.YValues[yValueHighIndex];
					}
					else if(dp.YValues[yValueLowIndex] < minPrice)
					{
						minPrice = dp.YValues[yValueLowIndex];
					}
				}
			}
		}
 
		/// <summary>
		/// Gets box size of the renko chart.
		/// </summary>
		/// <param name="series">Range column chart series used to dispaly the renko chart.</param>
		/// <param name="minPrice">Max price.</param>
		/// <param name="maxPrice">Min price.</param>
		private static double GetBoxSize(
			Series series, 
			double minPrice,
			double maxPrice)
		{
			// Check "BoxSize" custom attribute
			double	boxSize = 1.0;
			double	percentOfPriceRange = 4.0;
			bool	roundBoxSize = true;
            if (series.IsCustomPropertySet(CustomPropertyName.BoxSize))
            {
                string attrValue = series[CustomPropertyName.BoxSize].Trim();
                bool usePercentage = attrValue.EndsWith("%", StringComparison.Ordinal);
                if (usePercentage)
                {
                    attrValue = attrValue.Substring(0, attrValue.Length - 1);
                }
 
                bool parseSucceed = false;
                if (usePercentage)
                {
                    double percent;
                    parseSucceed = double.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out percent);
                    if (parseSucceed)
                    {
                        percentOfPriceRange = percent;
                        roundBoxSize = false;
                    }
                }
                else
                {
                    double b = 0;
                    parseSucceed = double.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out b);
                    if (parseSucceed)
                    {
                        boxSize = b;
                        percentOfPriceRange = 0.0;
                    }
                }
                if (!parseSucceed)
                {
                    throw (new InvalidOperationException(SR.ExceptionRenkoBoxSizeFormatInvalid));
                }
            }
 
			// Calculate box size using the percentage of price range
			if(percentOfPriceRange > 0.0)
			{
				// Set default box size
				boxSize = 1.0;
 
				// Calculate box size as percentage of price difference
				if(minPrice == maxPrice)
				{
					boxSize = 1.0;
				}
				else if( (maxPrice - minPrice) < 0.000001)
				{
					boxSize = 0.000001;
				}
				else
				{
					boxSize = (maxPrice - minPrice) * (percentOfPriceRange / 100.0);
				}
 
 
				// Round calculated value
				if(roundBoxSize)
				{
 
					double[] availableBoxSizes = new double[] 
						{ 0.000001, 0.00001, 0.0001, 0.001, 0.01, 0.1, 0.25, 0.5, 1.0, 2.0, 2.5, 3.0, 4.0, 5.0, 7.5, 10.0, 15.0, 20.0, 25.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 5000.0, 10000.0, 50000.0, 100000.0, 1000000.0, 1000000.0};
 
					for(int index = 1; index < availableBoxSizes.Length; index ++)
					{
						if(boxSize > availableBoxSizes[index - 1] &&
							boxSize < availableBoxSizes[index])
						{
							boxSize = availableBoxSizes[index];
						}
					}
				}
			}
 
			// Save current box size as a custom attribute of the original series
			series["CurrentBoxSize"] = boxSize.ToString(CultureInfo.InvariantCulture);
 
			return boxSize;
		}
 
		/// <summary>
		/// Gets reversal amount of the pointAndFigure chart.
		/// </summary>
		/// <param name="series">Step line chart series used to dispaly the pointAndFigure chart.</param>
		private static double GetReversalAmount(
			Series series)
		{
			// Check "ReversalAmount" custom attribute
			double	reversalAmount = 3.0;
            if (series.IsCustomPropertySet(CustomPropertyName.ReversalAmount))
            {
                string attrValue = series[CustomPropertyName.ReversalAmount].Trim();
 
                double amount;
                bool parseSucceed = double.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out amount);
                if (parseSucceed)
                {
                    reversalAmount = amount;
                }
                else
                {
                    throw (new InvalidOperationException(SR.ExceptionPointAndFigureReversalAmountInvalidFormat));
                }
            }
 
			return reversalAmount;
		}
 
 
		/// <summary>
		/// Fills step line series with data to draw the PointAndFigure chart.
		/// </summary>
		/// <param name="series">Step line chart series used to dispaly the PointAndFigure chart.</param>
		/// <param name="originalData">Series with original data.</param>
		private static void FillPointAndFigureData(Series series, Series originalData)
		{
			// Get index of the Y values used for High/Low
			int	yValueHighIndex = 0;
			if(series.IsCustomPropertySet(CustomPropertyName.UsedYValueHigh))
			{
				try
				{
 
					yValueHighIndex = int.Parse(series[CustomPropertyName.UsedYValueHigh], CultureInfo.InvariantCulture);
				}
				catch
				{
                    throw (new InvalidOperationException(SR.ExceptionPointAndFigureUsedYValueHighInvalidFormat));
				}
 
				if(yValueHighIndex >= series.YValuesPerPoint)
				{
                    throw (new InvalidOperationException(SR.ExceptionPointAndFigureUsedYValueHighOutOfRange));
				}
			}
			int	yValueLowIndex = 1;
			if(series.IsCustomPropertySet(CustomPropertyName.UsedYValueLow))
			{
				try
				{
					yValueLowIndex = int.Parse(series[CustomPropertyName.UsedYValueLow], CultureInfo.InvariantCulture);
				}
				catch
				{
                    throw (new InvalidOperationException(SR.ExceptionPointAndFigureUsedYValueLowInvalidFormat));
				}
 
				if(yValueLowIndex >= series.YValuesPerPoint)
				{
                    throw (new InvalidOperationException(SR.ExceptionPointAndFigureUsedYValueLowOutOfrange));
				}
			}
 
			// Get Up Brick color
			Color	upPriceColor = ChartGraphics.GetGradientColor(series.Color, Color.Black, 0.5);
			string	upPriceColorString = series[CustomPropertyName.PriceUpColor];
			if(upPriceColorString != null)
			{
				try
				{
					ColorConverter colorConverter = new ColorConverter();
					upPriceColor = (Color)colorConverter.ConvertFromString(null, CultureInfo.InvariantCulture, upPriceColorString);
				}
				catch
				{
                    throw (new InvalidOperationException(SR.ExceptionPointAndFigureUpBrickColorInvalidFormat));
				}
			}
 
			// Get price range
			double	priceHigh, priceLow;
			GetPriceRange(originalData, yValueHighIndex, yValueLowIndex, out priceHigh, out priceLow);
 
			// Calculate box size
			double	boxSize = GetBoxSize(series, priceHigh, priceLow);
 
			// Calculate reversal amount
			double	reversalAmount = GetReversalAmount(series);
 
			// Fill points
			double	prevHigh = double.NaN;
			double	prevLow = double.NaN;
			int		prevDirection = 0;	// 1 up; -1 down; 0 none
			int		pointIndex = 0;
			foreach(DataPoint dataPoint in originalData.Points)
			{
				if(!dataPoint.IsEmpty)
				{
                    // Indicates that all updates are already performed and no further processing required
                    bool    doNotUpdate = false;    
 
                    // Number of brciks total or added to the curent column
					int		numberOfBricks = 0;
 
					// Check if previus values exists
					if(double.IsNaN(prevHigh))
					{
						prevHigh = dataPoint.YValues[yValueHighIndex];
						prevLow = dataPoint.YValues[yValueLowIndex];
						++pointIndex;
						continue;
					}
 
					// Check direction of the price change
					int direction = 0;
					if(prevDirection == 1 || prevDirection == 0)
					{
						if(dataPoint.YValues[yValueHighIndex] >= (prevHigh + boxSize))
						{
							direction = 1;
							numberOfBricks = (int)Math.Floor(
								(dataPoint.YValues[yValueHighIndex] - prevHigh) / boxSize);
						}
						else if(dataPoint.YValues[yValueLowIndex] <= (prevHigh - boxSize * reversalAmount))
						{
							direction = -1;
							numberOfBricks = (int)Math.Floor(
								(prevHigh - dataPoint.YValues[yValueLowIndex]) / boxSize);
						}
                            // Adjust the lower part of the column while going up
                        else if (dataPoint.YValues[yValueHighIndex] <= (prevLow - boxSize))
                        {
                            doNotUpdate = true;
                            numberOfBricks = (int)Math.Floor(
                                (prevLow - dataPoint.YValues[yValueHighIndex]) / boxSize);
 
                            if (series.Points.Count > 0)
                            {
                                series.Points[series.Points.Count - 1].YValues[0] -= numberOfBricks * boxSize;
                            }
                            prevLow -= numberOfBricks * boxSize;
                        }
 
                    }
					if(direction == 0 &&
						(prevDirection == -1 || prevDirection == 0) )
					{
						if(dataPoint.YValues[yValueLowIndex] <= (prevLow - boxSize))
						{
							direction = -1;
							numberOfBricks = (int)Math.Floor(
								(prevLow - dataPoint.YValues[yValueLowIndex]) / boxSize);
						}
						else if(dataPoint.YValues[yValueHighIndex] >= (prevLow + boxSize * reversalAmount))
						{
							direction = 1;
							numberOfBricks = (int)Math.Floor(
								(dataPoint.YValues[yValueHighIndex] - prevLow) / boxSize);
						}
                        // Adjust the upper part of the column while going down
                        else if (dataPoint.YValues[yValueLowIndex] >= (prevHigh + boxSize))
                        {
                            doNotUpdate = true;
                            numberOfBricks = (int)Math.Floor(
                                (prevHigh - dataPoint.YValues[yValueLowIndex]) / boxSize);
 
                            if (series.Points.Count > 0)
                            {
                                series.Points[series.Points.Count - 1].YValues[1] += numberOfBricks * boxSize;
                            }
                            prevHigh += numberOfBricks * boxSize;
                        }
 
					}
 
					// Check if value was changed - otherwise do nothing
                    if (direction != 0 && !doNotUpdate)
					{
						// Extend line in same direction
						if(direction == prevDirection)
						{
                            if (direction == 1)
							{
                                series.Points[series.Points.Count - 1].YValues[1] += numberOfBricks * boxSize;
								prevHigh += numberOfBricks * boxSize;
								series.Points[series.Points.Count - 1]["OriginalPointIndex"] = pointIndex.ToString(CultureInfo.InvariantCulture);
							}
							else
							{
                                series.Points[series.Points.Count - 1].YValues[0] -= numberOfBricks * boxSize;
                                prevLow -= numberOfBricks * boxSize;
                                series.Points[series.Points.Count - 1]["OriginalPointIndex"] = pointIndex.ToString(CultureInfo.InvariantCulture);
							}
						}
						else
						{
							// Opposite direction by more than reversal amount
							DataPoint newDataPoint = (DataPoint)dataPoint.Clone();
							newDataPoint["OriginalPointIndex"] = pointIndex.ToString(CultureInfo.InvariantCulture);
							newDataPoint.series = series;
							newDataPoint.XValue = dataPoint.XValue;
							if(direction == 1)
							{
								newDataPoint.Color = upPriceColor;
								newDataPoint["PriceUpPoint"] = "true";
								newDataPoint.YValues[0] = prevLow + ((prevDirection != 0) ? boxSize : 0.0);
                                newDataPoint.YValues[1] = newDataPoint.YValues[0] + numberOfBricks * boxSize - ((prevDirection != 0) ? boxSize : 0.0);
							}
							else
							{
								newDataPoint.YValues[1] = prevHigh - ((prevDirection != 0) ? boxSize : 0.0);
								newDataPoint.YValues[0] = newDataPoint.YValues[1] - numberOfBricks * boxSize;
                            }
 
                            prevHigh = newDataPoint.YValues[1];
                            prevLow = newDataPoint.YValues[0];
 
							// Add PointAndFigure to the range step line series
							series.Points.Add(newDataPoint);
						}
 
                        // Save previous close value and direction
						prevDirection = direction;
					}
				}
				++pointIndex;
			}
 
		}
 
		/// <summary>
		/// Customize chart event, used to add empty points to make point and
		/// figure chart symbols look proportional.
		/// </summary>
        /// <param name="sender">The source Chart object of this event.</param>
        /// <param name="e">The EventArgs object that contains the event data.</param>
		static private void OnCustomize(Object sender, EventArgs e)
		{
			bool	chartResized = false;
            Chart chart = (Chart)sender;
			// Loop through all series
			foreach(Series series in chart.Series)
			{
				// Check for the PointAndFigure chart type
				if(series.Name.StartsWith("POINTANDFIGURE_ORIGINAL_DATA_", StringComparison.Ordinal))
				{
					// Get original series
					Series	pointAndFigureSeries = chart.Series[series.Name.Substring(29)];
 
					// Check if proportional symbol custom attribute is set
					bool	proportionalSymbols = true;
					string	attrValue = pointAndFigureSeries[CustomPropertyName.ProportionalSymbols];
					if(attrValue != null && String.Compare( attrValue, "True", StringComparison.OrdinalIgnoreCase ) != 0 )
					{
						proportionalSymbols = false;
					}
 
					if(proportionalSymbols && 
						pointAndFigureSeries.Enabled && 
						pointAndFigureSeries.ChartArea.Length > 0)
					{
						// Resize chart
						if(!chartResized)
						{
							chartResized = true;
							chart.chartPicture.Resize(chart.chartPicture.ChartGraph, false);
						}
 
						// Find series chart area, X & Y axes
						ChartArea	chartArea = chart.ChartAreas[pointAndFigureSeries.ChartArea];
						Axis		axisX = chartArea.GetAxis(AxisName.X, pointAndFigureSeries.XAxisType, pointAndFigureSeries.XSubAxisName);
						Axis		axisY = chartArea.GetAxis(AxisName.Y, pointAndFigureSeries.YAxisType, pointAndFigureSeries.YSubAxisName);
 
						// Symbols are drawn only in 2D mode
						if(!chartArea.Area3DStyle.Enable3D)
						{
							// Get current box size
							double boxSize = double.Parse(
								pointAndFigureSeries["CurrentBoxSize"],
								CultureInfo.InvariantCulture);
 
							// Calculate symbol width and height
							double boxYSize = Math.Abs(
								axisY.GetPosition(axisY.Minimum) - 
								axisY.GetPosition(axisY.Minimum + boxSize) );
							double boxXSize = Math.Abs(
								axisX.GetPosition(1.0) - 
								axisX.GetPosition(0.0) );
							boxXSize *= 0.8;
 
							// Get absolute size in pixels
							SizeF markSize = chart.chartPicture.ChartGraph.GetAbsoluteSize(
								new SizeF((float)boxXSize, (float)boxYSize));
 
							// Calculate number of empty points that should be added
							int pointCount = 0;
							if(markSize.Width > markSize.Height)
							{
								pointCount = (int)(pointAndFigureSeries.Points.Count * (markSize.Width / markSize.Height));
							}
 
							// Add empty points
							DataPoint emptyPoint = new DataPoint(pointAndFigureSeries);
							emptyPoint.IsEmpty = true;
							emptyPoint.AxisLabel = " ";
							while(pointAndFigureSeries.Points.Count < pointCount)
							{
								pointAndFigureSeries.Points.Add(emptyPoint);
							}
 
							// Always use zeros for Y values of empty points
							pointAndFigureSeries[CustomPropertyName.EmptyPointValue] = "Zero";
		
							// RecalculateAxesScale chart are data
							chartArea.ReCalcInternal();
						}
					}
				}
			}
		}
 
		#endregion // Methods
 
		#region Drawing methods
		
		/// <summary>
		/// Draws 2D column using 'X' or 'O' symbols.
		/// </summary>
		/// <param name="graph">Chart graphics.</param>
		/// <param name="vAxis">Vertical axis.</param>
		/// <param name="rectSize">Column position and size.</param>
		/// <param name="point">Column data point.</param>
		/// <param name="ser">Column series.</param>
		protected override void DrawColumn2D( 
			ChartGraphics graph,
			Axis vAxis,
			RectangleF rectSize,
			DataPoint point, 
			Series ser)
		{
			// Get box size
            double boxSize = double.Parse(ser["CurrentBoxSize"], CultureInfo.InvariantCulture);
			double boxSizeRel = vAxis.GetLogValue(vAxis.ViewMinimum);
			boxSizeRel = vAxis.GetLinearPosition(boxSizeRel);
			boxSizeRel = Math.Abs(boxSizeRel - 
				vAxis.GetLinearPosition(vAxis.GetLogValue(vAxis.ViewMinimum + boxSize)));
 
			// Draw a series of Xs or Os
			for(float positionY = rectSize.Y; positionY < rectSize.Bottom - (float)(boxSizeRel - boxSizeRel/4.0); positionY += (float)boxSizeRel)
			{
				// Get position of symbol
				RectangleF	position = RectangleF.Empty;
				position.X = rectSize.X;
				position.Y = positionY;
				position.Width = rectSize.Width;
				position.Height = (float)boxSizeRel;
 
				// Get absolute position and add 1 pixel spacing
				position = graph.GetAbsoluteRectangle(position);
				int	spacing = 1 + point.BorderWidth / 2;
				position.Y += spacing;
				position.Height -= 2 * spacing;
 
				// Calculate shadow position
				RectangleF	shadowPosition = new RectangleF(position.Location, position.Size);
				shadowPosition.Offset(ser.ShadowOffset, ser.ShadowOffset);
 
				if(point.IsCustomPropertySet("PriceUpPoint"))
				{
					// Draw shadow
					if(ser.ShadowOffset != 0)
					{
						graph.DrawLineAbs( 
							ser.ShadowColor, 
							point.BorderWidth, 
							ChartDashStyle.Solid, 
							new PointF(shadowPosition.Left, shadowPosition.Top),
							new PointF(shadowPosition.Right, shadowPosition.Bottom));
						graph.DrawLineAbs( 
							ser.ShadowColor, 
							point.BorderWidth, 
							ChartDashStyle.Solid, 
							new PointF(shadowPosition.Left, shadowPosition.Bottom),
							new PointF(shadowPosition.Right, shadowPosition.Top));
					}
					
					// Draw 'X' symbol
					graph.DrawLineAbs( 
						point.Color, 
						point.BorderWidth, 
						ChartDashStyle.Solid, 
						new PointF(position.Left, position.Top),
						new PointF(position.Right, position.Bottom));
					graph.DrawLineAbs( 
						point.Color, 
						point.BorderWidth, 
						ChartDashStyle.Solid, 
						new PointF(position.Left, position.Bottom),
						new PointF(position.Right, position.Top));
				}
				else
				{
					// Draw circles when price is dropping
					if(ser.ShadowOffset != 0)
					{
						graph.DrawCircleAbs(
							new Pen(ser.ShadowColor, point.BorderWidth), 
							null, 
							shadowPosition, 
							1, 
							false);
					}
 
					// Draw 'O' symbol
					graph.DrawCircleAbs(
						new Pen(point.Color, point.BorderWidth), 
						null, 
						position, 
						1, 
						false);
				}
			}
 
 
		}
 
		#endregion // Drawing methods
 
		#region IChartType interface implementation
 
		/// <summary>
		/// Chart type name
		/// </summary>
		override public string Name			{ get{ return ChartTypeNames.PointAndFigure;}}
 
		/// <summary>
		/// Gets chart type image.
		/// </summary>
		/// <param name="registry">Chart types registry object.</param>
		/// <returns>Chart type image.</returns>
		override public System.Drawing.Image GetImage(ChartTypeRegistry registry)
		{
			return (System.Drawing.Image)registry.ResourceManager.GetObject(this.Name + "ChartType");
		}
		#endregion
	}
}