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