1 //-------------------------------------------------------------
2 // <copyright company=�Microsoft Corporation�>
3 //   Copyright � Microsoft Corporation. All Rights Reserved.
4 // </copyright>
5 //-------------------------------------------------------------
6 // @owner=alexgor, deliant
7 //=================================================================
8 //  File:		PointAndFigureChart.cs
9 //
10 //  Namespace:	DataVisualization.Charting.ChartTypes
11 //
12 //	Classes:	PointAndFigureChart
13 //
14 //  Purpose:	Point and Figure chart type do not plot series data
15 //              point directly as most of the other chart types.
16 //              Instead it uses different calculation to create a
17 //              new RangeColumn type series based on its data in
18 //              the PrepareData method. All the changes in this
19 //              method are reversed back in the UnPrepareData
20 //              method. RangeColumn chart type is extended to
21 //              display a column of Os or Xs.
22 //
23 //	Point and Figure Charts Overview:
24 //  ---------------------------------
25 //
26 //	Point and Figure charts differ from traditional price charts in
27 //	that they completely disregard the passage of time, and only
28 //	display changes in prices. Rather than having price on the y-axis,
29 //	and time on the x-axis, Point and Figure charts display price
30 //	changes on both axes. This is similar to the Kagi, Renko, and
31 //	Three Line Break charts.
32 //
33 //	The Point and Figure chart displays the underlying supply and
34 //	demand as reflected in the price values. A column of Xs shows
35 //	that demand is exceeding supply, which is known as a rally,
36 //	a column of Os shows that supply is exceeding demand, which is
37 //	known as a decline, and a series of short columns shows that
38 //	supply and demand are relatively equal, which of course,
39 //	represents a market equilibrium.
40 //
41 //	The following should be taken into account when working with
42 //	this type of chart:
43 //
44 //	- The X values of data points are automatically indexed. For
45 //	more information see the topic on Indexing Data Point X Values.
46 //
47 //	- There is a formula applied to the original data before it gets
48 //	plotted. This formula changes the number of points, as well as
49 //	their X/Y values.
50 //
51 //	- Due to the data being recalculated, we do not recommend setting
52 //	the minimum, or maximum values for the X axis. This is because it
53 //	cannot be determined how many data points will actually be plotted.
54 //	However,  if the axis' Maximum, or Minimum is set, then the Maximum,
55 //	and Minimum properties will use data point index values.
56 //
57 //	- Data point anchoring, used for annotations, is not supported
58 //	with this type of chart.
59 //
60 //	Reviewed:   AG - Microsoft 6, 2007
61 //
62 //===================================================================
63 
64 #region Used namespaces
65 
66 using System;
67 using System.Resources;
68 using System.Reflection;
69 using System.Collections;
70 using System.Drawing;
71 using System.Drawing.Drawing2D;
72 using System.ComponentModel.Design;
73 using System.Globalization;
74 
75 #if Microsoft_CONTROL
76 	using System.Windows.Forms.DataVisualization.Charting;
77 	using System.Windows.Forms.DataVisualization.Charting.Data;
78 	using System.Windows.Forms.DataVisualization.Charting.ChartTypes;
79 	using System.Windows.Forms.DataVisualization.Charting.Utilities;
80 	using System.Windows.Forms.DataVisualization.Charting.Borders3D;
81 #else
82 using System.Web.UI.DataVisualization.Charting;
83 
84 using System.Web.UI.DataVisualization.Charting.ChartTypes;
85 using System.Web.UI.DataVisualization.Charting.Data;
86 using System.Web.UI.DataVisualization.Charting.Utilities;
87 #endif
88 
89 #endregion
90 
91 #if Microsoft_CONTROL
92 	namespace System.Windows.Forms.DataVisualization.Charting.ChartTypes
93 #else
94 namespace System.Web.UI.DataVisualization.Charting.ChartTypes
95 #endif
96 {
97 	/// <summary>
98     /// PointAndFigureChart class contains all the code necessary for calculation
99     /// and drawing Point and Figure chart.
100 	/// </summary>
101 	internal class PointAndFigureChart : RangeColumnChart
102 	{
103 		#region Fields
104 
105 		/// <summary>
106 		/// Indicates that class subscribed fro the customize event.
107 		/// </summary>
108 		static private	bool	_customizeSubscribed = false;
109 
110 		#endregion // Fields
111 
112 		#region Methods
113 
114 		/// <summary>
115 		/// Prepares PointAndFigure chart type for rendering. We hide original series
116         /// during rendering and only using the data for calculations. New RangeColumn
117         /// type series is added wich displayes the columns of Os or Xs.
118         /// All the changes in this method are reversed back in the UnPrepareData method.
119 		/// </summary>
120 		/// <param name="series">Series to be prepared.</param>
PrepareData(Series series)121 		internal static void PrepareData(Series series)
122 		{
123 			// Check series chart type
124 			if(String.Compare( series.ChartTypeName, ChartTypeNames.PointAndFigure, StringComparison.OrdinalIgnoreCase ) != 0 || !series.IsVisible())
125 			{
126 				return;
127 			}
128 
129 			// Get reference to the chart control
130 			Chart	chart = series.Chart;
131 			if(chart == null)
132 			{
133                 throw (new InvalidOperationException(SR.ExceptionPointAndFigureNullReference));
134 			}
135 
136             // PointAndFigure chart may not be combined with any other chart types
137             ChartArea area = chart.ChartAreas[series.ChartArea];
138             foreach (Series currentSeries in chart.Series)
139             {
140                 if (currentSeries.IsVisible() && currentSeries != series && area == chart.ChartAreas[currentSeries.ChartArea])
141                 {
142                     throw (new InvalidOperationException(SR.ExceptionPointAndFigureCanNotCombine));
143                 }
144             }
145 
146 			// Subscribe for customize event
147 			if(!_customizeSubscribed)
148 			{
149 				_customizeSubscribed = true;
150 				chart.Customize += new EventHandler(OnCustomize);
151 			}
152 
153 			// Create a temp series which will hold original series data points
154 			string tempSeriesName = "POINTANDFIGURE_ORIGINAL_DATA_" + series.Name;
155 			if (chart.Series.IndexOf(tempSeriesName) != -1)
156 			{
157 				return; // the temp series has already been added
158 			}
159 			Series seriesOriginalData = new Series(tempSeriesName, series.YValuesPerPoint);
160 			seriesOriginalData.Enabled = false;
161 			seriesOriginalData.IsVisibleInLegend = false;
162 			seriesOriginalData.YValuesPerPoint = series.YValuesPerPoint;
163 			chart.Series.Add(seriesOriginalData);
164 			foreach(DataPoint dp in series.Points)
165 			{
166 				seriesOriginalData.Points.Add(dp);
167 			}
168 			series.Points.Clear();
169 			if(series.IsCustomPropertySet("TempDesignData"))
170 			{
171 				seriesOriginalData["TempDesignData"] = "true";
172 			}
173 
174 
175 			// Remember prev. series parameters
176 			series["OldXValueIndexed"] = series.IsXValueIndexed.ToString(CultureInfo.InvariantCulture);
177 			series["OldYValuesPerPoint"] = series.YValuesPerPoint.ToString(CultureInfo.InvariantCulture);
178 			series.IsXValueIndexed = true;
179 
180 			// Calculate date-time interval for indexed series
181 			if(series.ChartArea.Length > 0 &&
182 				series.IsXValueDateTime())
183 			{
184 				// Get X axis connected to the series
185 				Axis		xAxis = area.GetAxis(AxisName.X, series.XAxisType, series.XSubAxisName);
186 
187 				// Change interval for auto-calculated interval only
188 				if(xAxis.Interval == 0 && xAxis.IntervalType == DateTimeIntervalType.Auto)
189 				{
190 					// Check if original data has X values set to date-time values and
191 					// calculate min/max X values.
192 					bool	nonZeroXValues = false;
193 					double	minX = double.MaxValue;
194 					double	maxX = double.MinValue;
195 					foreach(DataPoint dp in seriesOriginalData.Points)
196 					{
197 						if(!dp.IsEmpty)
198 						{
199 							if(dp.XValue != 0.0)
200 							{
201 								nonZeroXValues = true;
202 							}
203 							if(dp.XValue > maxX)
204 							{
205 								maxX = dp.XValue;
206 							}
207 							if(dp.XValue < minX)
208 							{
209 								minX = dp.XValue;
210 							}
211 						}
212 					}
213 
214 					if(nonZeroXValues)
215 					{
216 						// Save flag that axis interval is automatic
217 						series["OldAutomaticXAxisInterval"] = "true";
218 
219 						// Calculate and set axis date-time interval
220 						DateTimeIntervalType	intervalType = DateTimeIntervalType.Auto;
221 						xAxis.interval = xAxis.CalcInterval(minX, maxX, true, out intervalType, series.XValueType);
222 						xAxis.intervalType = intervalType;
223 					}
224 				}
225 			}
226 
227 			// Calculate PointAndFigure bricks data points values
228 			FillPointAndFigureData(series, seriesOriginalData);
229 		}
230 
231 		/// <summary>
232 		/// Remove any changes done while preparing PointAndFigure chart type for rendering.
233 		/// </summary>
234 		/// <param name="series">Series to be un-prepared.</param>
235 		/// <returns>True if series was removed from collection.</returns>
UnPrepareData(Series series)236 		internal static bool UnPrepareData(Series series)
237 		{
238 			if(series.Name.StartsWith("POINTANDFIGURE_ORIGINAL_DATA_", StringComparison.Ordinal))
239 			{
240 				// Get reference to the chart control
241 				Chart	chart = series.Chart;
242 				if(chart == null)
243 				{
244                     throw (new InvalidOperationException(SR.ExceptionPointAndFigureNullReference));
245 				}
246 
247 				// Unsubscribe for customize event
248 				if(_customizeSubscribed)
249 				{
250 					_customizeSubscribed = false;
251 					chart.Customize -= new EventHandler(OnCustomize);
252 				}
253 
254 				// Get original PointAndFigure series
255 				Series	pointAndFigureSeries = chart.Series[series.Name.Substring(29)];
256                 Series.MovePositionMarkers(pointAndFigureSeries, series);
257 
258 				// Copy data back to original PointAndFigure series
259 				pointAndFigureSeries.Points.Clear();
260 				if(!series.IsCustomPropertySet("TempDesignData"))
261 				{
262 					foreach(DataPoint dp in series.Points)
263 					{
264 						pointAndFigureSeries.Points.Add(dp);
265 					}
266 				}
267 
268 				// Restore series properties
269                 bool xValIndexed;
270                 bool parseSucceed = bool.TryParse(pointAndFigureSeries["OldXValueIndexed"], out xValIndexed);
271 
272                 pointAndFigureSeries.IsXValueIndexed = parseSucceed && xValIndexed;
273 
274                 int yVals;
275                 parseSucceed = int.TryParse(pointAndFigureSeries["OldYValuesPerPoint"], NumberStyles.Any, CultureInfo.InvariantCulture, out yVals);
276 
277                 if (parseSucceed)
278                 {
279                     pointAndFigureSeries.YValuesPerPoint = yVals;
280                 }
281 
282 				pointAndFigureSeries.DeleteCustomProperty("OldXValueIndexed");
283 				pointAndFigureSeries.DeleteCustomProperty("OldYValuesPerPoint");
284 				pointAndFigureSeries.DeleteCustomProperty(CustomPropertyName.EmptyPointValue);
285 
286 				series["OldAutomaticXAxisInterval"] = "true";
287 				if(pointAndFigureSeries.IsCustomPropertySet("OldAutomaticXAxisInterval"))
288 				{
289 					pointAndFigureSeries.DeleteCustomProperty("OldAutomaticXAxisInterval");
290 
291 					// Reset automatic interval for X axis
292 					if(pointAndFigureSeries.ChartArea.Length > 0)
293 					{
294 						// Get X axis connected to the series
295 						ChartArea	area = chart.ChartAreas[pointAndFigureSeries.ChartArea];
296 						Axis		xAxis = area.GetAxis(AxisName.X, pointAndFigureSeries.XAxisType, pointAndFigureSeries.XSubAxisName);
297 
298 						xAxis.interval = 0.0;
299 						xAxis.intervalType = DateTimeIntervalType.Auto;
300 					}
301 				}
302 
303 				// Remove series from the collection
304 				chart.Series.Remove(series);
305 				return true;
306 			}
307 
308 			return false;
309 		}
310 
311 		/// <summary>
312 		/// Gets price range in the point and figure chart.
313 		/// </summary>
314 		/// <param name="originalData">Series with original data.</param>
315 		/// <param name="yValueHighIndex">Index of the Y value to use as High price.</param>
316 		/// <param name="yValueLowIndex">Index of the Y value to use as Low price.</param>
317 		/// <param name="minPrice">Returns max price.</param>
318 		/// <param name="maxPrice">Returns min price.</param>
GetPriceRange( Series originalData, int yValueHighIndex, int yValueLowIndex, out double minPrice, out double maxPrice)319 		private static void GetPriceRange(
320 			Series originalData,
321 			int yValueHighIndex,
322 			int yValueLowIndex,
323 			out double minPrice,
324 			out double maxPrice)
325 		{
326 			// Calculate percent of the highest and lowest price difference.
327 			maxPrice = double.MinValue;
328 			minPrice = double.MaxValue;
329 			foreach(DataPoint dp in originalData.Points)
330 			{
331 				if(!dp.IsEmpty)
332 				{
333 					// Check required Y values number
334 					if(dp.YValues.Length < 2)
335 					{
336 						throw(new InvalidOperationException(SR.ExceptionChartTypeRequiresYValues(ChartTypeNames.PointAndFigure, ((int)(2)).ToString(CultureInfo.CurrentCulture))));
337 					}
338 
339 					if(dp.YValues[yValueHighIndex] > maxPrice)
340 					{
341 						maxPrice = dp.YValues[yValueHighIndex];
342 					}
343 					else if(dp.YValues[yValueLowIndex] > maxPrice)
344 					{
345 						maxPrice = dp.YValues[yValueLowIndex];
346 					}
347 
348 					if(dp.YValues[yValueHighIndex] < minPrice)
349 					{
350 						minPrice = dp.YValues[yValueHighIndex];
351 					}
352 					else if(dp.YValues[yValueLowIndex] < minPrice)
353 					{
354 						minPrice = dp.YValues[yValueLowIndex];
355 					}
356 				}
357 			}
358 		}
359 
360 		/// <summary>
361 		/// Gets box size of the renko chart.
362 		/// </summary>
363 		/// <param name="series">Range column chart series used to dispaly the renko chart.</param>
364 		/// <param name="minPrice">Max price.</param>
365 		/// <param name="maxPrice">Min price.</param>
GetBoxSize( Series series, double minPrice, double maxPrice)366 		private static double GetBoxSize(
367 			Series series,
368 			double minPrice,
369 			double maxPrice)
370 		{
371 			// Check "BoxSize" custom attribute
372 			double	boxSize = 1.0;
373 			double	percentOfPriceRange = 4.0;
374 			bool	roundBoxSize = true;
375             if (series.IsCustomPropertySet(CustomPropertyName.BoxSize))
376             {
377                 string attrValue = series[CustomPropertyName.BoxSize].Trim();
378                 bool usePercentage = attrValue.EndsWith("%", StringComparison.Ordinal);
379                 if (usePercentage)
380                 {
381                     attrValue = attrValue.Substring(0, attrValue.Length - 1);
382                 }
383 
384                 bool parseSucceed = false;
385                 if (usePercentage)
386                 {
387                     double percent;
388                     parseSucceed = double.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out percent);
389                     if (parseSucceed)
390                     {
391                         percentOfPriceRange = percent;
392                         roundBoxSize = false;
393                     }
394                 }
395                 else
396                 {
397                     double b = 0;
398                     parseSucceed = double.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out b);
399                     if (parseSucceed)
400                     {
401                         boxSize = b;
402                         percentOfPriceRange = 0.0;
403                     }
404                 }
405                 if (!parseSucceed)
406                 {
407                     throw (new InvalidOperationException(SR.ExceptionRenkoBoxSizeFormatInvalid));
408                 }
409             }
410 
411 			// Calculate box size using the percentage of price range
412 			if(percentOfPriceRange > 0.0)
413 			{
414 				// Set default box size
415 				boxSize = 1.0;
416 
417 				// Calculate box size as percentage of price difference
418 				if(minPrice == maxPrice)
419 				{
420 					boxSize = 1.0;
421 				}
422 				else if( (maxPrice - minPrice) < 0.000001)
423 				{
424 					boxSize = 0.000001;
425 				}
426 				else
427 				{
428 					boxSize = (maxPrice - minPrice) * (percentOfPriceRange / 100.0);
429 				}
430 
431 
432 				// Round calculated value
433 				if(roundBoxSize)
434 				{
435 
436 					double[] availableBoxSizes = new double[]
437 						{ 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};
438 
439 					for(int index = 1; index < availableBoxSizes.Length; index ++)
440 					{
441 						if(boxSize > availableBoxSizes[index - 1] &&
442 							boxSize < availableBoxSizes[index])
443 						{
444 							boxSize = availableBoxSizes[index];
445 						}
446 					}
447 				}
448 			}
449 
450 			// Save current box size as a custom attribute of the original series
451 			series["CurrentBoxSize"] = boxSize.ToString(CultureInfo.InvariantCulture);
452 
453 			return boxSize;
454 		}
455 
456 		/// <summary>
457 		/// Gets reversal amount of the pointAndFigure chart.
458 		/// </summary>
459 		/// <param name="series">Step line chart series used to dispaly the pointAndFigure chart.</param>
GetReversalAmount( Series series)460 		private static double GetReversalAmount(
461 			Series series)
462 		{
463 			// Check "ReversalAmount" custom attribute
464 			double	reversalAmount = 3.0;
465             if (series.IsCustomPropertySet(CustomPropertyName.ReversalAmount))
466             {
467                 string attrValue = series[CustomPropertyName.ReversalAmount].Trim();
468 
469                 double amount;
470                 bool parseSucceed = double.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out amount);
471                 if (parseSucceed)
472                 {
473                     reversalAmount = amount;
474                 }
475                 else
476                 {
477                     throw (new InvalidOperationException(SR.ExceptionPointAndFigureReversalAmountInvalidFormat));
478                 }
479             }
480 
481 			return reversalAmount;
482 		}
483 
484 
485 		/// <summary>
486 		/// Fills step line series with data to draw the PointAndFigure chart.
487 		/// </summary>
488 		/// <param name="series">Step line chart series used to dispaly the PointAndFigure chart.</param>
489 		/// <param name="originalData">Series with original data.</param>
FillPointAndFigureData(Series series, Series originalData)490 		private static void FillPointAndFigureData(Series series, Series originalData)
491 		{
492 			// Get index of the Y values used for High/Low
493 			int	yValueHighIndex = 0;
494 			if(series.IsCustomPropertySet(CustomPropertyName.UsedYValueHigh))
495 			{
496 				try
497 				{
498 
499 					yValueHighIndex = int.Parse(series[CustomPropertyName.UsedYValueHigh], CultureInfo.InvariantCulture);
500 				}
501 				catch
502 				{
503                     throw (new InvalidOperationException(SR.ExceptionPointAndFigureUsedYValueHighInvalidFormat));
504 				}
505 
506 				if(yValueHighIndex >= series.YValuesPerPoint)
507 				{
508                     throw (new InvalidOperationException(SR.ExceptionPointAndFigureUsedYValueHighOutOfRange));
509 				}
510 			}
511 			int	yValueLowIndex = 1;
512 			if(series.IsCustomPropertySet(CustomPropertyName.UsedYValueLow))
513 			{
514 				try
515 				{
516 					yValueLowIndex = int.Parse(series[CustomPropertyName.UsedYValueLow], CultureInfo.InvariantCulture);
517 				}
518 				catch
519 				{
520                     throw (new InvalidOperationException(SR.ExceptionPointAndFigureUsedYValueLowInvalidFormat));
521 				}
522 
523 				if(yValueLowIndex >= series.YValuesPerPoint)
524 				{
525                     throw (new InvalidOperationException(SR.ExceptionPointAndFigureUsedYValueLowOutOfrange));
526 				}
527 			}
528 
529 			// Get Up Brick color
530 			Color	upPriceColor = ChartGraphics.GetGradientColor(series.Color, Color.Black, 0.5);
531 			string	upPriceColorString = series[CustomPropertyName.PriceUpColor];
532 			if(upPriceColorString != null)
533 			{
534 				try
535 				{
536 					ColorConverter colorConverter = new ColorConverter();
537 					upPriceColor = (Color)colorConverter.ConvertFromString(null, CultureInfo.InvariantCulture, upPriceColorString);
538 				}
539 				catch
540 				{
541                     throw (new InvalidOperationException(SR.ExceptionPointAndFigureUpBrickColorInvalidFormat));
542 				}
543 			}
544 
545 			// Get price range
546 			double	priceHigh, priceLow;
547 			GetPriceRange(originalData, yValueHighIndex, yValueLowIndex, out priceHigh, out priceLow);
548 
549 			// Calculate box size
550 			double	boxSize = GetBoxSize(series, priceHigh, priceLow);
551 
552 			// Calculate reversal amount
553 			double	reversalAmount = GetReversalAmount(series);
554 
555 			// Fill points
556 			double	prevHigh = double.NaN;
557 			double	prevLow = double.NaN;
558 			int		prevDirection = 0;	// 1 up; -1 down; 0 none
559 			int		pointIndex = 0;
560 			foreach(DataPoint dataPoint in originalData.Points)
561 			{
562 				if(!dataPoint.IsEmpty)
563 				{
564                     // Indicates that all updates are already performed and no further processing required
565                     bool    doNotUpdate = false;
566 
567                     // Number of brciks total or added to the curent column
568 					int		numberOfBricks = 0;
569 
570 					// Check if previus values exists
571 					if(double.IsNaN(prevHigh))
572 					{
573 						prevHigh = dataPoint.YValues[yValueHighIndex];
574 						prevLow = dataPoint.YValues[yValueLowIndex];
575 						++pointIndex;
576 						continue;
577 					}
578 
579 					// Check direction of the price change
580 					int direction = 0;
581 					if(prevDirection == 1 || prevDirection == 0)
582 					{
583 						if(dataPoint.YValues[yValueHighIndex] >= (prevHigh + boxSize))
584 						{
585 							direction = 1;
586 							numberOfBricks = (int)Math.Floor(
587 								(dataPoint.YValues[yValueHighIndex] - prevHigh) / boxSize);
588 						}
589 						else if(dataPoint.YValues[yValueLowIndex] <= (prevHigh - boxSize * reversalAmount))
590 						{
591 							direction = -1;
592 							numberOfBricks = (int)Math.Floor(
593 								(prevHigh - dataPoint.YValues[yValueLowIndex]) / boxSize);
594 						}
595                             // Adjust the lower part of the column while going up
596                         else if (dataPoint.YValues[yValueHighIndex] <= (prevLow - boxSize))
597                         {
598                             doNotUpdate = true;
599                             numberOfBricks = (int)Math.Floor(
600                                 (prevLow - dataPoint.YValues[yValueHighIndex]) / boxSize);
601 
602                             if (series.Points.Count > 0)
603                             {
604                                 series.Points[series.Points.Count - 1].YValues[0] -= numberOfBricks * boxSize;
605                             }
606                             prevLow -= numberOfBricks * boxSize;
607                         }
608 
609                     }
610 					if(direction == 0 &&
611 						(prevDirection == -1 || prevDirection == 0) )
612 					{
613 						if(dataPoint.YValues[yValueLowIndex] <= (prevLow - boxSize))
614 						{
615 							direction = -1;
616 							numberOfBricks = (int)Math.Floor(
617 								(prevLow - dataPoint.YValues[yValueLowIndex]) / boxSize);
618 						}
619 						else if(dataPoint.YValues[yValueHighIndex] >= (prevLow + boxSize * reversalAmount))
620 						{
621 							direction = 1;
622 							numberOfBricks = (int)Math.Floor(
623 								(dataPoint.YValues[yValueHighIndex] - prevLow) / boxSize);
624 						}
625                         // Adjust the upper part of the column while going down
626                         else if (dataPoint.YValues[yValueLowIndex] >= (prevHigh + boxSize))
627                         {
628                             doNotUpdate = true;
629                             numberOfBricks = (int)Math.Floor(
630                                 (prevHigh - dataPoint.YValues[yValueLowIndex]) / boxSize);
631 
632                             if (series.Points.Count > 0)
633                             {
634                                 series.Points[series.Points.Count - 1].YValues[1] += numberOfBricks * boxSize;
635                             }
636                             prevHigh += numberOfBricks * boxSize;
637                         }
638 
639 					}
640 
641 					// Check if value was changed - otherwise do nothing
642                     if (direction != 0 && !doNotUpdate)
643 					{
644 						// Extend line in same direction
645 						if(direction == prevDirection)
646 						{
647                             if (direction == 1)
648 							{
649                                 series.Points[series.Points.Count - 1].YValues[1] += numberOfBricks * boxSize;
650 								prevHigh += numberOfBricks * boxSize;
651 								series.Points[series.Points.Count - 1]["OriginalPointIndex"] = pointIndex.ToString(CultureInfo.InvariantCulture);
652 							}
653 							else
654 							{
655                                 series.Points[series.Points.Count - 1].YValues[0] -= numberOfBricks * boxSize;
656                                 prevLow -= numberOfBricks * boxSize;
657                                 series.Points[series.Points.Count - 1]["OriginalPointIndex"] = pointIndex.ToString(CultureInfo.InvariantCulture);
658 							}
659 						}
660 						else
661 						{
662 							// Opposite direction by more than reversal amount
663 							DataPoint newDataPoint = (DataPoint)dataPoint.Clone();
664 							newDataPoint["OriginalPointIndex"] = pointIndex.ToString(CultureInfo.InvariantCulture);
665 							newDataPoint.series = series;
666 							newDataPoint.XValue = dataPoint.XValue;
667 							if(direction == 1)
668 							{
669 								newDataPoint.Color = upPriceColor;
670 								newDataPoint["PriceUpPoint"] = "true";
671 								newDataPoint.YValues[0] = prevLow + ((prevDirection != 0) ? boxSize : 0.0);
672                                 newDataPoint.YValues[1] = newDataPoint.YValues[0] + numberOfBricks * boxSize - ((prevDirection != 0) ? boxSize : 0.0);
673 							}
674 							else
675 							{
676 								newDataPoint.YValues[1] = prevHigh - ((prevDirection != 0) ? boxSize : 0.0);
677 								newDataPoint.YValues[0] = newDataPoint.YValues[1] - numberOfBricks * boxSize;
678                             }
679 
680                             prevHigh = newDataPoint.YValues[1];
681                             prevLow = newDataPoint.YValues[0];
682 
683 							// Add PointAndFigure to the range step line series
684 							series.Points.Add(newDataPoint);
685 						}
686 
687                         // Save previous close value and direction
688 						prevDirection = direction;
689 					}
690 				}
691 				++pointIndex;
692 			}
693 
694 		}
695 
696 		/// <summary>
697 		/// Customize chart event, used to add empty points to make point and
698 		/// figure chart symbols look proportional.
699 		/// </summary>
700         /// <param name="sender">The source Chart object of this event.</param>
701         /// <param name="e">The EventArgs object that contains the event data.</param>
OnCustomize(Object sender, EventArgs e)702 		static private void OnCustomize(Object sender, EventArgs e)
703 		{
704 			bool	chartResized = false;
705             Chart chart = (Chart)sender;
706 			// Loop through all series
707 			foreach(Series series in chart.Series)
708 			{
709 				// Check for the PointAndFigure chart type
710 				if(series.Name.StartsWith("POINTANDFIGURE_ORIGINAL_DATA_", StringComparison.Ordinal))
711 				{
712 					// Get original series
713 					Series	pointAndFigureSeries = chart.Series[series.Name.Substring(29)];
714 
715 					// Check if proportional symbol custom attribute is set
716 					bool	proportionalSymbols = true;
717 					string	attrValue = pointAndFigureSeries[CustomPropertyName.ProportionalSymbols];
718 					if(attrValue != null && String.Compare( attrValue, "True", StringComparison.OrdinalIgnoreCase ) != 0 )
719 					{
720 						proportionalSymbols = false;
721 					}
722 
723 					if(proportionalSymbols &&
724 						pointAndFigureSeries.Enabled &&
725 						pointAndFigureSeries.ChartArea.Length > 0)
726 					{
727 						// Resize chart
728 						if(!chartResized)
729 						{
730 							chartResized = true;
731 							chart.chartPicture.Resize(chart.chartPicture.ChartGraph, false);
732 						}
733 
734 						// Find series chart area, X & Y axes
735 						ChartArea	chartArea = chart.ChartAreas[pointAndFigureSeries.ChartArea];
736 						Axis		axisX = chartArea.GetAxis(AxisName.X, pointAndFigureSeries.XAxisType, pointAndFigureSeries.XSubAxisName);
737 						Axis		axisY = chartArea.GetAxis(AxisName.Y, pointAndFigureSeries.YAxisType, pointAndFigureSeries.YSubAxisName);
738 
739 						// Symbols are drawn only in 2D mode
740 						if(!chartArea.Area3DStyle.Enable3D)
741 						{
742 							// Get current box size
743 							double boxSize = double.Parse(
744 								pointAndFigureSeries["CurrentBoxSize"],
745 								CultureInfo.InvariantCulture);
746 
747 							// Calculate symbol width and height
748 							double boxYSize = Math.Abs(
749 								axisY.GetPosition(axisY.Minimum) -
750 								axisY.GetPosition(axisY.Minimum + boxSize) );
751 							double boxXSize = Math.Abs(
752 								axisX.GetPosition(1.0) -
753 								axisX.GetPosition(0.0) );
754 							boxXSize *= 0.8;
755 
756 							// Get absolute size in pixels
757 							SizeF markSize = chart.chartPicture.ChartGraph.GetAbsoluteSize(
758 								new SizeF((float)boxXSize, (float)boxYSize));
759 
760 							// Calculate number of empty points that should be added
761 							int pointCount = 0;
762 							if(markSize.Width > markSize.Height)
763 							{
764 								pointCount = (int)(pointAndFigureSeries.Points.Count * (markSize.Width / markSize.Height));
765 							}
766 
767 							// Add empty points
768 							DataPoint emptyPoint = new DataPoint(pointAndFigureSeries);
769 							emptyPoint.IsEmpty = true;
770 							emptyPoint.AxisLabel = " ";
771 							while(pointAndFigureSeries.Points.Count < pointCount)
772 							{
773 								pointAndFigureSeries.Points.Add(emptyPoint);
774 							}
775 
776 							// Always use zeros for Y values of empty points
777 							pointAndFigureSeries[CustomPropertyName.EmptyPointValue] = "Zero";
778 
779 							// RecalculateAxesScale chart are data
780 							chartArea.ReCalcInternal();
781 						}
782 					}
783 				}
784 			}
785 		}
786 
787 		#endregion // Methods
788 
789 		#region Drawing methods
790 
791 		/// <summary>
792 		/// Draws 2D column using 'X' or 'O' symbols.
793 		/// </summary>
794 		/// <param name="graph">Chart graphics.</param>
795 		/// <param name="vAxis">Vertical axis.</param>
796 		/// <param name="rectSize">Column position and size.</param>
797 		/// <param name="point">Column data point.</param>
798 		/// <param name="ser">Column series.</param>
DrawColumn2D( ChartGraphics graph, Axis vAxis, RectangleF rectSize, DataPoint point, Series ser)799 		protected override void DrawColumn2D(
800 			ChartGraphics graph,
801 			Axis vAxis,
802 			RectangleF rectSize,
803 			DataPoint point,
804 			Series ser)
805 		{
806 			// Get box size
807             double boxSize = double.Parse(ser["CurrentBoxSize"], CultureInfo.InvariantCulture);
808 			double boxSizeRel = vAxis.GetLogValue(vAxis.ViewMinimum);
809 			boxSizeRel = vAxis.GetLinearPosition(boxSizeRel);
810 			boxSizeRel = Math.Abs(boxSizeRel -
811 				vAxis.GetLinearPosition(vAxis.GetLogValue(vAxis.ViewMinimum + boxSize)));
812 
813 			// Draw a series of Xs or Os
814 			for(float positionY = rectSize.Y; positionY < rectSize.Bottom - (float)(boxSizeRel - boxSizeRel/4.0); positionY += (float)boxSizeRel)
815 			{
816 				// Get position of symbol
817 				RectangleF	position = RectangleF.Empty;
818 				position.X = rectSize.X;
819 				position.Y = positionY;
820 				position.Width = rectSize.Width;
821 				position.Height = (float)boxSizeRel;
822 
823 				// Get absolute position and add 1 pixel spacing
824 				position = graph.GetAbsoluteRectangle(position);
825 				int	spacing = 1 + point.BorderWidth / 2;
826 				position.Y += spacing;
827 				position.Height -= 2 * spacing;
828 
829 				// Calculate shadow position
830 				RectangleF	shadowPosition = new RectangleF(position.Location, position.Size);
831 				shadowPosition.Offset(ser.ShadowOffset, ser.ShadowOffset);
832 
833 				if(point.IsCustomPropertySet("PriceUpPoint"))
834 				{
835 					// Draw shadow
836 					if(ser.ShadowOffset != 0)
837 					{
838 						graph.DrawLineAbs(
839 							ser.ShadowColor,
840 							point.BorderWidth,
841 							ChartDashStyle.Solid,
842 							new PointF(shadowPosition.Left, shadowPosition.Top),
843 							new PointF(shadowPosition.Right, shadowPosition.Bottom));
844 						graph.DrawLineAbs(
845 							ser.ShadowColor,
846 							point.BorderWidth,
847 							ChartDashStyle.Solid,
848 							new PointF(shadowPosition.Left, shadowPosition.Bottom),
849 							new PointF(shadowPosition.Right, shadowPosition.Top));
850 					}
851 
852 					// Draw 'X' symbol
853 					graph.DrawLineAbs(
854 						point.Color,
855 						point.BorderWidth,
856 						ChartDashStyle.Solid,
857 						new PointF(position.Left, position.Top),
858 						new PointF(position.Right, position.Bottom));
859 					graph.DrawLineAbs(
860 						point.Color,
861 						point.BorderWidth,
862 						ChartDashStyle.Solid,
863 						new PointF(position.Left, position.Bottom),
864 						new PointF(position.Right, position.Top));
865 				}
866 				else
867 				{
868 					// Draw circles when price is dropping
869 					if(ser.ShadowOffset != 0)
870 					{
871 						graph.DrawCircleAbs(
872 							new Pen(ser.ShadowColor, point.BorderWidth),
873 							null,
874 							shadowPosition,
875 							1,
876 							false);
877 					}
878 
879 					// Draw 'O' symbol
880 					graph.DrawCircleAbs(
881 						new Pen(point.Color, point.BorderWidth),
882 						null,
883 						position,
884 						1,
885 						false);
886 				}
887 			}
888 
889 
890 		}
891 
892 		#endregion // Drawing methods
893 
894 		#region IChartType interface implementation
895 
896 		/// <summary>
897 		/// Chart type name
898 		/// </summary>
899 		override public string Name			{ get{ return ChartTypeNames.PointAndFigure;}}
900 
901 		/// <summary>
902 		/// Gets chart type image.
903 		/// </summary>
904 		/// <param name="registry">Chart types registry object.</param>
905 		/// <returns>Chart type image.</returns>
GetImage(ChartTypeRegistry registry)906 		override public System.Drawing.Image GetImage(ChartTypeRegistry registry)
907 		{
908 			return (System.Drawing.Image)registry.ResourceManager.GetObject(this.Name + "ChartType");
909 		}
910 		#endregion
911 	}
912 }
913 
914