File: Common\ChartTypes\RenkoChart.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:		RenkoChart.cs
//
//  Namespace:	DataVisualization.Charting.ChartTypes
//
//	Classes:	RenkoChart
//
//  Purpose:	Renko chart type provides methods for calculations and
//				depends on the Range Column chart type to do all the 
//				drawing. PrepareData method is used to create temporary
//				RangeColumn series and fill it with data. Changes are 
//				then reversed in the UnPrepareData method.
//
//	Renko Chart Overview:
//	---------------------
//	The Renko charting method is thought to have acquired its name 
//	from Renga, a Japanese word for bricks. Renko charts isolate the 
//	underlying price trends by filtering out minor price changes. 
//	These charts can be very useful for determining major trend lines, 
//	or support and resistance levels.
//	
//	Basic trend reversals are signaled with the emergence of a new 
//	color brick which depends on the choice of colors used in the 
//	series. Since the Renko chart is used as a trend following aid, 
//	there are times when Renko charts produce whip saws, giving 
//	signals near the end of short-lived trends. However, the 
//	expectation with any trend following technique is that it allows 
//	you to ride the major portion of any significant trends.
//	
//	Renko charts are normally based on closing price values. However, 
//	unless otherwise specified, the value reflected in the chart will 
//	be the first YValue. You can also specify a box size that 
//	determines the minimum price change to display in the chart. The 
//	default box size is calculated from the average share price over 
//	the charted period.
 
//	The following should be taken into account when working with Renko 
//	charts:
//	
//	- The X values of data points are automatically indexed. 
//	
//	- There is a formula applied to the original data before that data 
//	gets plotted. This formula changes the number of points in the data, 
//	and also changes the X and Y values of the data points. 
//	
//	- Due to data being recalculated, we do not recommend setting the 
//	minimum and/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, 
//	or Minimum properties should use data point index values. 
//	
//	- Data point anchoring, used for annotations, is not supported in 
//	this type of chart. 
//	
//	Reviewed:	AG - Microsoft 7, 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>
    /// RenkoChart class provides methods to perform all nessesary 
    /// calculations to display Renko chart with the help of the 
    /// temporary RangeColumn series. This series is created in the 
    /// PrepareData method and then removed in the UnPrepareData method.
	/// </summary>
	internal class RenkoChart : IChartType
	{
		#region Methods
 
		/// <summary>
		/// Prepares renko chart type for rendering.
		/// </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.Renko, StringComparison.OrdinalIgnoreCase ) != 0 || !series.IsVisible())
			{
				return;
			}
 
			// Get reference to the chart control
			Chart	chart = series.Chart;
			if(chart == null)
			{
                throw (new InvalidOperationException(SR.ExceptionRenkoNullReference));
			}
 
            // Renko 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.ExceptionRenkoCanNotCobine));
                }
            }
 
 
			// Create a temp series which will hold original series data points
			Series seriesOriginalData = new Series("RENKO_ORIGINAL_DATA_" + series.Name, series.YValuesPerPoint);
			seriesOriginalData.Enabled = false;
			seriesOriginalData.IsVisibleInLegend = false;
			chart.Series.Add(seriesOriginalData);
			foreach(DataPoint dp in series.Points)
			{
				seriesOriginalData.Points.Add(dp);
			}
			series.Points.Clear();
			if(series.IsCustomPropertySet("TempDesignData"))
			{
				seriesOriginalData["TempDesignData"] = "true";
			}
 
 
			// Change renko series type to range column
			series["OldXValueIndexed"] = series.IsXValueIndexed.ToString(CultureInfo.InvariantCulture);
			series["OldYValuesPerPoint"] = series.YValuesPerPoint.ToString(CultureInfo.InvariantCulture);
			series.ChartType = SeriesChartType.RangeColumn;
			series.IsXValueIndexed = true;
			series.YValuesPerPoint = 2;
 
			// 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 renko bricks data points values
			FillRenkoData(series, seriesOriginalData);
		}
 
		/// <summary>
		/// Remove any changes done while preparing renko 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("RENKO_ORIGINAL_DATA_", StringComparison.Ordinal))
            {
                // Get reference to the chart control
                Chart chart = series.Chart;
                if (chart == null)
                {
                    throw (new InvalidOperationException(SR.ExceptionRenkoNullReference));
                }
 
                // Get original Renko series
                Series renkoSeries = chart.Series[series.Name.Substring(20)];
                Series.MovePositionMarkers(renkoSeries, series);
                // Copy data back to original Renko series
                renkoSeries.Points.Clear();
                if (!series.IsCustomPropertySet("TempDesignData"))
                {
                    foreach (DataPoint dp in series.Points)
                    {
                        renkoSeries.Points.Add(dp);
                    }
                }
 
                // Restore renko series properties
                renkoSeries.ChartType = SeriesChartType.Renko;
 
                bool isXValIndexed;
                bool parseSucceed = bool.TryParse(renkoSeries["OldXValueIndexed"], out isXValIndexed);
                renkoSeries.IsXValueIndexed = parseSucceed && isXValIndexed;
 
                int yValsPerPoint;
                parseSucceed = int.TryParse(renkoSeries["OldYValuesPerPoint"], NumberStyles.Any, CultureInfo.InvariantCulture, out yValsPerPoint);
 
                if (parseSucceed)
                {
                    renkoSeries.YValuesPerPoint = yValsPerPoint;
                }
 
                renkoSeries.DeleteCustomProperty("OldXValueIndexed");
                renkoSeries.DeleteCustomProperty("OldYValuesPerPoint");
 
                series["OldAutomaticXAxisInterval"] = "true";
                if (renkoSeries.IsCustomPropertySet("OldAutomaticXAxisInterval"))
                {
                    renkoSeries.DeleteCustomProperty("OldAutomaticXAxisInterval");
 
                    // Reset automatic interval for X axis
                    if (renkoSeries.ChartArea.Length > 0)
                    {
                        // Get X axis connected to the series
                        ChartArea area = chart.ChartAreas[renkoSeries.ChartArea];
                        Axis xAxis = area.GetAxis(AxisName.X, renkoSeries.XAxisType, renkoSeries.XSubAxisName);
 
                        xAxis.interval = 0.0;
                        xAxis.intervalType = DateTimeIntervalType.Auto;
                    }
                }
 
                // Remove series from the collection
                chart.Series.Remove(series);
                return true;
            }
 
			// Remove current box size attribute
			if(series.IsCustomPropertySet("CurrentBoxSize"))
			{
				series.DeleteCustomProperty("CurrentBoxSize");
			}
 
			return false;
		}
 
		/// <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="originalData">Series with original data.</param>
		/// <param name="yValueIndex">Index of the Y value to use.</param>
		private static double GetBoxSize(Series series, Series originalData, int yValueIndex)
		{
			// 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);
				}
 
				try
				{
					if(usePercentage)
					{
						percentOfPriceRange = double.Parse(attrValue, CultureInfo.InvariantCulture);
						roundBoxSize = false;
					}
					else
					{
						boxSize = double.Parse(attrValue, CultureInfo.InvariantCulture);
						percentOfPriceRange = 0.0;
					}
				}
				catch
				{
                    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 percent of the highest and lowest price difference.
				double highest = double.MinValue;
				double lowest = double.MaxValue;
				foreach(DataPoint dp in originalData.Points)
				{
					if(!dp.IsEmpty)
					{
						if(dp.YValues[yValueIndex] > highest)
						{
							highest = dp.YValues[yValueIndex];
						}
						if(dp.YValues[yValueIndex] < lowest)
						{
							lowest = dp.YValues[yValueIndex];
						}
					}
				}
 
				// Calculate box size as percentage of price difference
				if(lowest == highest)
				{
					boxSize = 1.0;
				}
				else if( (highest - lowest) < 0.000001)
				{
					boxSize = 0.000001;
				}
				else
				{
					boxSize = (highest - lowest) * (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>
		/// Fills range column series with data to draw the renko chart.
		/// </summary>
		/// <param name="series">Range column chart series used to dispaly the renko chart.</param>
		/// <param name="originalData">Series with original data.</param>
		private static void FillRenkoData(Series series, Series originalData)
		{
			// Get index of the Y values used
			int	yValueIndex = 0;
			if(series.IsCustomPropertySet(CustomPropertyName.UsedYValue))
			{
				try
				{
					yValueIndex = int.Parse(series[CustomPropertyName.UsedYValue], CultureInfo.InvariantCulture);
				}
				catch
				{
                    throw (new InvalidOperationException(SR.ExceptionRenkoUsedYValueFormatInvalid));
				}
 
				if(yValueIndex >= series.YValuesPerPoint)
				{
                    throw (new InvalidOperationException(SR.ExceptionRenkoUsedYValueOutOfRange));
				}
			}
 
			// Calculate box size
			double	boxSize = GetBoxSize(series, originalData, yValueIndex);
 
			// Fill points
			double	prevLow = double.NaN;
			double	prevHigh = double.NaN;
			int		pointIndex = 0;
			foreach(DataPoint dataPoint in originalData.Points)
			{
				if(!dataPoint.IsEmpty)
				{
					int		numberOfBricks = 0;
					bool	goingUp = true;
 
					// Check if previus values exists
					if(double.IsNaN(prevLow) || double.IsNaN(prevHigh))
					{
						prevHigh = dataPoint.YValues[yValueIndex];
						prevLow = dataPoint.YValues[yValueIndex];
						++pointIndex;
						continue;
					}
 
					// Get Up Brick color
					Color	upBrickColor = Color.Transparent;
					string	upBrickColorString = dataPoint[CustomPropertyName.PriceUpColor];
					if(upBrickColorString == null)
					{
						upBrickColorString = series[CustomPropertyName.PriceUpColor];
					}
					if(upBrickColorString != null)
					{
						try
						{
							ColorConverter colorConverter = new ColorConverter();
							upBrickColor = (Color)colorConverter.ConvertFromString(null, CultureInfo.InvariantCulture, upBrickColorString);
						}
						catch
						{
                            throw (new InvalidOperationException(SR.ExceptionRenkoUpBrickColorInvalid));
						}
					}
 
					// Check if close value exceeds last brick position by box size
					if(dataPoint.YValues[yValueIndex] >= (prevHigh + boxSize))
					{
						goingUp = true;
						numberOfBricks = (int)Math.Floor((dataPoint.YValues[yValueIndex] - prevHigh) / boxSize);
					}
					else if(dataPoint.YValues[yValueIndex] <= (prevLow - boxSize))
					{
						goingUp = false;
						numberOfBricks = (int)Math.Floor((prevLow - dataPoint.YValues[yValueIndex]) / boxSize);
					}
 
					// Add points
					while(numberOfBricks > 0)
					{
						// Create new point
						DataPoint newDataPoint = (DataPoint)dataPoint.Clone();
						newDataPoint["OriginalPointIndex"] = pointIndex.ToString(CultureInfo.InvariantCulture);
						newDataPoint.series = series;
						newDataPoint.YValues = new double[2];
						newDataPoint.XValue = dataPoint.XValue;
                        newDataPoint.Tag = dataPoint;
						if(goingUp)
						{
							newDataPoint.YValues[1] = prevHigh;
							newDataPoint.YValues[0] = prevHigh + boxSize;
							prevLow = prevHigh;
							prevHigh = prevLow + boxSize;
 
							// Set renko up brick appearance
							newDataPoint.Color = upBrickColor;
							if(newDataPoint.BorderWidth < 1)
							{
								newDataPoint.BorderWidth = 1;
							}
							if(newDataPoint.BorderDashStyle == ChartDashStyle.NotSet)
							{
								newDataPoint.BorderDashStyle = ChartDashStyle.Solid;
							}
							if( (newDataPoint.BorderColor == Color.Empty || newDataPoint.BorderColor == Color.Transparent) &&
								(newDataPoint.Color == Color.Empty || newDataPoint.Color == Color.Transparent) )
							{
								newDataPoint.BorderColor = series.Color;
							}
						}
						else
						{
							newDataPoint.YValues[1] = prevLow;
							newDataPoint.YValues[0] = prevLow - boxSize;
							prevHigh = prevLow;
							prevLow = prevHigh - boxSize;
						}
 
						// Add renko brick to the range column series
						series.Points.Add(newDataPoint);
						--numberOfBricks;
					}
				}
				++pointIndex;
			}
		}
 
		#endregion // Methods
 
		#region Painting and Selection methods
 
		/// <summary>
		/// Paint stock chart.
		/// </summary>
		/// <param name="graph">The Chart Graphics object.</param>
		/// <param name="common">The Common elements object.</param>
		/// <param name="area">Chart area for this chart.</param>
		/// <param name="seriesToDraw">Chart series to draw.</param>
		virtual public void Paint( ChartGraphics graph, CommonElements common, ChartArea area, Series seriesToDraw )
		{
            // Renko series is never drawn directly. It is replaced with the range column chart. 
            // See PrepareData method.
		}
 
		#endregion
 
		#region IChartType interface implementation
 
		/// <summary>
		/// Chart type name
		/// </summary>
		virtual public string Name			{ get{ return ChartTypeNames.Renko;}}
 
		/// <summary>
		/// True if chart type is stacked
		/// </summary>
		virtual public bool Stacked		{ get{ return false;}}
 
 
		/// <summary>
		/// True if stacked chart type supports groups
		/// </summary>
		virtual public bool SupportStackedGroups	{ get { return false; } }
 
 
		/// <summary>
		/// True if stacked chart type should draw separately positive and 
		/// negative data points ( Bar and column Stacked types ).
		/// </summary>
		public bool StackSign		{ get{ return false;}}
 
		/// <summary>
		/// True if chart type supports axeses
		/// </summary>
		virtual public bool RequireAxes	{ get{ return true;} }
 
		/// <summary>
		/// Chart type with two y values used for scale ( bubble chart type )
		/// </summary>
		public bool SecondYScale{ get{ return false;} }
 
		/// <summary>
		/// True if chart type requires circular chart area.
		/// </summary>
		public bool CircularChartArea	{ get{ return false;} }
 
		/// <summary>
		/// True if chart type supports logarithmic axes
		/// </summary>
		virtual public bool SupportLogarithmicAxes	{ get{ return true;} }
 
		/// <summary>
		/// True if chart type requires to switch the value (Y) axes position
		/// </summary>
		virtual public bool SwitchValueAxes	{ get{ return false;} }
 
		/// <summary>
		/// True if chart series can be placed side-by-side.
		/// </summary>
		public bool SideBySideSeries { get{ return false;} }
 
		/// <summary>
		/// True if each data point of a chart must be represented in the legend
		/// </summary>
		virtual public bool DataPointsInLegend	{ get{ return false;} }
 
		/// <summary>
		/// If the crossing value is auto Crossing value should be 
		/// automatically set to zero for some chart 
		/// types (Bar, column, area etc.)
		/// </summary>
		virtual public bool ZeroCrossing { get{ return false;} }
 
		/// <summary>
		/// True if palette colors should be applied for each data paoint.
		/// Otherwise the color is applied to the series.
		/// </summary>
		virtual public bool ApplyPaletteColorsToPoints	{ get { return false; } }
 
		/// <summary>
		/// Indicates that extra Y values are connected to the scale of the Y axis
		/// </summary>
		virtual public bool ExtraYValuesConnectedToYAxis{ get { return true; } }
		
		/// <summary>
		/// Indicates that it's a hundredred percent chart.
		/// Axis scale from 0 to 100 percent should be used.
		/// </summary>
		virtual public bool HundredPercent{ get{return false;} }
 
		/// <summary>
		/// Indicates that it's a hundredred percent chart.
		/// Axis scale from 0 to 100 percent should be used.
		/// </summary>
		virtual public bool HundredPercentSupportNegative{ get{return false;} }
 
		/// <summary>
		/// How to draw series/points in legend:
		/// Filled rectangle, Line or Marker
		/// </summary>
		/// <param name="series">Legend item series.</param>
		/// <returns>Legend item style.</returns>
		virtual public LegendImageStyle GetLegendImageStyle(Series series)
		{
			return LegendImageStyle.Rectangle;
		}
	
		/// <summary>
		/// Number of supported Y value(s) per point 
		/// </summary>
		virtual public int YValuesPerPoint	{ get { return 1; } }
 
		/// <summary>
		/// Gets chart type image.
		/// </summary>
		/// <param name="registry">Chart types registry object.</param>
		/// <returns>Chart type image.</returns>
        virtual public System.Drawing.Image GetImage(ChartTypeRegistry registry)
		{
			return (System.Drawing.Image)registry.ResourceManager.GetObject(this.Name + "ChartType");
		}
		#endregion
 
		#region Y values related methods
 
		/// <summary>
		/// Helper function, which returns the Y value of the point.
		/// </summary>
		/// <param name="common">Chart common elements.</param>
		/// <param name="area">Chart area the series belongs to.</param>
		/// <param name="series">Sereis of the point.</param>
		/// <param name="point">Point object.</param>
		/// <param name="pointIndex">Index of the point.</param>
		/// <param name="yValueIndex">Index of the Y value to get.</param>
		/// <returns>Y value of the point.</returns>
		virtual public double GetYValue(
			CommonElements common, 
			ChartArea area, 
			Series series, 
			DataPoint point, 
			int pointIndex, 
			int yValueIndex)
		{
			return point.YValues[yValueIndex];
		}
 
		#endregion
 
		#region SmartLabelStyle methods
 
		/// <summary>
		/// Adds markers position to the list. Used to check SmartLabelStyle overlapping.
		/// </summary>
		/// <param name="common">Common chart elements.</param>
		/// <param name="area">Chart area.</param>
		/// <param name="series">Series values to be used.</param>
		/// <param name="list">List to add to.</param>
		public void AddSmartLabelMarkerPositions(CommonElements common, ChartArea area, Series series, ArrayList list)
		{
            // NOTE: SmartLabelStyle feature is not supported by this chart type.
		}
 
		#endregion
 
        #region IDisposable interface implementation
        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            //Nothing to dispose at the base class. 
        }
 
        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
	}
}