1 //------------------------------------------------------------- 2 // <copyright company=�Microsoft Corporation�> 3 // Copyright � Microsoft Corporation. All Rights Reserved. 4 // </copyright> 5 //------------------------------------------------------------- 6 // @owner=alexgor, deliant 7 //================================================================= 8 // File: FunnelChart.cs 9 // 10 // Namespace: DataVisualization.Charting.ChartTypes 11 // 12 // Classes: FunnelChart, PyramidChart, FunnelSegmentInfo, 13 // FunnelPointLabelInfo 14 // 15 // Purpose: Provides 2D/3D drawing and hit testing functionality 16 // for the Funnel and Pyramid charts. 17 // 18 // Funnel and Pyramid Chart types display data that 19 // equals 100% when totalled. This type of chart is a 20 // single series chart representing the data as portions 21 // of 100%, and this chart does not use any axes. 22 // 23 // Reviewed: AG - Microsoft 6, 2007 24 // 25 //=================================================================== 26 27 #region Used namespaces 28 29 using System; 30 using System.Collections; 31 using System.Drawing; 32 using System.Drawing.Drawing2D; 33 using System.Diagnostics.CodeAnalysis; 34 using System.Globalization; 35 36 #if Microsoft_CONTROL 37 using System.Windows.Forms.DataVisualization.Charting.Utilities; 38 #else 39 using System.Web.UI.DataVisualization.Charting.Utilities; 40 #endif 41 42 #endregion // Used namespaces 43 44 #if Microsoft_CONTROL 45 namespace System.Windows.Forms.DataVisualization.Charting.ChartTypes 46 #else // Microsoft_CONTROL 47 namespace System.Web.UI.DataVisualization.Charting.ChartTypes 48 #endif // Microsoft_CONTROL 49 { 50 #region Enumerations 51 52 /// <summary> 53 /// Value type of the pyramid chart. 54 /// </summary> 55 internal enum PyramidValueType 56 { 57 /// <summary> 58 /// Each point value defines linear height of each segment. 59 /// </summary> 60 Linear, 61 62 /// <summary> 63 /// Each point value defines surface of each segment. 64 /// </summary> 65 Surface 66 } 67 68 /// <summary> 69 /// Funnel chart drawing style. 70 /// </summary> 71 internal enum FunnelStyle 72 { 73 /// <summary> 74 /// Shape of the funnel is fixed and point Y value controls the height of the segments. 75 /// </summary> 76 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "YIs")] 77 YIsHeight, 78 79 /// <summary> 80 /// Height of each segment is the same and point Y value controls the diameter of the segment. 81 /// </summary> 82 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "YIs")] 83 YIsWidth 84 } 85 86 /// <summary> 87 /// Outside labels placement. 88 /// </summary> 89 internal enum FunnelLabelPlacement 90 { 91 /// <summary> 92 /// Labels are placed on the right side of the funnel. 93 /// </summary> 94 Right, 95 96 /// <summary> 97 /// Labels are placed on the left side of the funnel. 98 /// </summary> 99 Left 100 } 101 102 /// <summary> 103 /// Vertical alignment of the data point labels 104 /// </summary> 105 internal enum FunnelLabelVerticalAlignment 106 { 107 /// <summary> 108 /// Label placed in the middle. 109 /// </summary> 110 Center, 111 112 /// <summary> 113 /// Label placed on top. 114 /// </summary> 115 Top, 116 117 /// <summary> 118 /// Label placed on the bottom. 119 /// </summary> 120 Bottom 121 } 122 123 /// <summary> 124 /// Funnel chart 3D drawing style. 125 /// </summary> 126 internal enum Funnel3DDrawingStyle 127 { 128 /// <summary> 129 /// Circle will be used as a shape of the base. 130 /// </summary> 131 CircularBase, 132 133 /// <summary> 134 /// Square will be used as a shape of the base. 135 /// </summary> 136 SquareBase 137 } 138 139 140 /// <summary> 141 /// Funnel chart labels style enumeration. 142 /// </summary> 143 internal enum FunnelLabelStyle 144 { 145 /// <summary> 146 /// Data point labels are located inside of the funnel. 147 /// </summary> 148 Inside, 149 150 /// <summary> 151 /// Data point labels are located outside of the funnel. 152 /// </summary> 153 Outside, 154 155 /// <summary> 156 /// Data point labels are located outside of the funnel in a column. 157 /// </summary> 158 OutsideInColumn, 159 160 /// <summary> 161 /// Data point labels are disabled. 162 /// </summary> 163 Disabled 164 } 165 166 #endregion // Enumerations 167 168 /// <summary> 169 /// FunnelChart class provides 2D/3D drawing and hit testing functionality 170 /// for the Funnel and Pyramid charts. 171 /// </summary> 172 internal class FunnelChart : IChartType 173 { 174 #region Fields and Constructor 175 176 // Array list of funnel segments 177 internal ArrayList segmentList = null; 178 179 // List of data point labels information 180 internal ArrayList labelInfoList = null; 181 182 // Chart graphics object. 183 internal ChartGraphics Graph { get; set; } 184 185 // Chart area the chart type belongs to. 186 internal ChartArea Area { get; set; } 187 188 // Common chart elements. 189 internal CommonElements Common { get; set; } 190 191 // Spacing between each side of the funnel and chart area. 192 internal RectangleF plotAreaSpacing = new RectangleF(3f, 3f, 3f, 3f); 193 194 // Current chart type series 195 private Series _chartTypeSeries = null; 196 197 // Sum of all Y values in the data series 198 internal double yValueTotal = 0.0; 199 200 // Maximum Y value in the data series 201 private double _yValueMax = 0.0; 202 203 // Sum of all X values in the data series 204 private double _xValueTotal = 0.0; 205 206 // Number of points in the series 207 internal int pointNumber; 208 209 // Calculted plotting area of the chart 210 private RectangleF _plotAreaPosition = RectangleF.Empty; 211 212 // Funnel chart drawing style 213 private FunnelStyle _funnelStyle = FunnelStyle.YIsHeight; 214 215 // Define the shape of the funnel neck 216 private SizeF _funnelNeckSize = new SizeF(50f, 30f); 217 218 // Gap between funnel segments 219 internal float funnelSegmentGap = 0f; 220 221 // 3D funnel rotation angle 222 private int _rotation3D = 5; 223 224 // Indicates that rounded shape is used to draw 3D chart type instead of square 225 internal bool round3DShape = true; 226 227 // Indicates that Pyramid chart is rendered. 228 internal bool isPyramid = false; 229 230 // Minimum data point height 231 private float _funnelMinPointHeight = 0f; 232 233 // Name of the attribute that controls the height of the gap between the points 234 internal string funnelPointGapAttributeName = CustomPropertyName.FunnelPointGap; 235 236 // Name of the attribute that controls the 3D funnel rotation angle 237 internal string funnelRotationAngleAttributeName = CustomPropertyName.Funnel3DRotationAngle; 238 239 // Name of the attribute that controls the minimum height of the point 240 protected string funnelPointMinHeight = CustomPropertyName.FunnelMinPointHeight; 241 242 // Name of the attribute that controls the minimum height of the point 243 internal string funnel3DDrawingStyleAttributeName = CustomPropertyName.Funnel3DDrawingStyle; 244 245 // Name of the attribute that controls inside labels vertical alignment 246 internal string funnelInsideLabelAlignmentAttributeName = CustomPropertyName.FunnelInsideLabelAlignment; 247 248 // Name of the attribute that controls outside labels placement (Left vs. Right) 249 protected string funnelOutsideLabelPlacementAttributeName = CustomPropertyName.FunnelOutsideLabelPlacement; 250 251 // Name of the attribute that controls labels style 252 internal string funnelLabelStyleAttributeName = CustomPropertyName.FunnelLabelStyle; 253 254 // Array of data point value adjusments in percentage 255 private double[] _valuePercentages = null; 256 257 /// <summary> 258 /// Default constructor 259 /// </summary> FunnelChart()260 public FunnelChart() 261 { 262 } 263 264 #endregion 265 266 #region Properties 267 268 /// <summary> 269 /// Gets or sets the calculted plotting area of the chart 270 /// </summary> 271 internal RectangleF PlotAreaPosition 272 { 273 get { return _plotAreaPosition; } 274 set { _plotAreaPosition = value; } 275 } 276 277 #endregion // Properties 278 279 #region IChartType interface implementation 280 281 /// <summary> 282 /// Chart type name 283 /// </summary> 284 virtual public string Name { get{ return ChartTypeNames.Funnel;}} 285 286 /// <summary> 287 /// True if chart type is stacked 288 /// </summary> 289 virtual public bool Stacked { get{ return false;}} 290 291 292 /// <summary> 293 /// True if stacked chart type supports groups 294 /// </summary> 295 virtual public bool SupportStackedGroups { get { return false; } } 296 297 298 /// <summary> 299 /// True if stacked chart type should draw separately positive and 300 /// negative data points ( Bar and column Stacked types ). 301 /// </summary> 302 public bool StackSign { get{ return false;}} 303 304 /// <summary> 305 /// True if chart type supports axeses 306 /// </summary> 307 virtual public bool RequireAxes { get{ return false;} } 308 309 /// <summary> 310 /// Chart type with two y values used for scale ( bubble chart type ) 311 /// </summary> 312 virtual public bool SecondYScale{ get{ return false;} } 313 314 /// <summary> 315 /// True if chart type requires circular chart area. 316 /// </summary> 317 public bool CircularChartArea { get{ return false;} } 318 319 /// <summary> 320 /// True if chart type supports logarithmic axes 321 /// </summary> 322 virtual public bool SupportLogarithmicAxes { get{ return true;} } 323 324 /// <summary> 325 /// True if chart type requires to switch the value (Y) axes position 326 /// </summary> 327 virtual public bool SwitchValueAxes { get{ return false;} } 328 329 /// <summary> 330 /// True if chart series can be placed side-by-side. 331 /// </summary> 332 virtual public bool SideBySideSeries { get{ return false;} } 333 334 /// <summary> 335 /// True if each data point of a chart must be represented in the legend 336 /// </summary> 337 virtual public bool DataPointsInLegend { get{ return true;} } 338 339 /// <summary> 340 /// If the crossing value is auto Crossing value should be 341 /// automatically set to zero for some chart 342 /// types (Bar, column, area etc.) 343 /// </summary> 344 virtual public bool ZeroCrossing { get{ return false;} } 345 346 /// <summary> 347 /// True if palette colors should be applied for each data paoint. 348 /// Otherwise the color is applied to the series. 349 /// </summary> 350 virtual public bool ApplyPaletteColorsToPoints { get { return true; } } 351 352 /// <summary> 353 /// Indicates that extra Y values are connected to the scale of the Y axis 354 /// </summary> 355 virtual public bool ExtraYValuesConnectedToYAxis{ get { return false; } } 356 357 /// <summary> 358 /// Indicates that it's a hundredred percent chart. 359 /// Axis scale from 0 to 100 percent should be used. 360 /// </summary> 361 virtual public bool HundredPercent{ get{return false;} } 362 363 /// <summary> 364 /// Indicates that it's a hundredred percent chart. 365 /// Axis scale from 0 to 100 percent should be used. 366 /// </summary> 367 virtual public bool HundredPercentSupportNegative{ get{return false;} } 368 369 /// <summary> 370 /// How to draw series/points in legend: 371 /// Filled rectangle, Line or Marker 372 /// </summary> 373 /// <param name="series">Legend item series.</param> 374 /// <returns>Legend item style.</returns> GetLegendImageStyle(Series series)375 virtual public LegendImageStyle GetLegendImageStyle(Series series) 376 { 377 return LegendImageStyle.Rectangle; 378 } 379 380 /// <summary> 381 /// Number of supported Y value(s) per point 382 /// </summary> 383 virtual public int YValuesPerPoint { get { return 1; } } 384 385 /// <summary> 386 /// Gets chart type image. 387 /// </summary> 388 /// <param name="registry">Chart types registry object.</param> 389 /// <returns>Chart type image.</returns> GetImage(ChartTypeRegistry registry)390 virtual public System.Drawing.Image GetImage(ChartTypeRegistry registry) 391 { 392 return (System.Drawing.Image)registry.ResourceManager.GetObject(this.Name + "ChartType"); 393 } 394 395 #endregion 396 397 #region Painting 398 399 /// <summary> 400 /// Paint Funnel Chart. 401 /// </summary> 402 /// <param name="graph">The Chart Graphics object.</param> 403 /// <param name="common">The Common elements object.</param> 404 /// <param name="area">Chart area for this chart.</param> 405 /// <param name="seriesToDraw">Chart series to draw.</param> Paint( ChartGraphics graph, CommonElements common, ChartArea area, Series seriesToDraw )406 virtual public void Paint( 407 ChartGraphics graph, 408 CommonElements common, 409 ChartArea area, 410 Series seriesToDraw ) 411 { 412 // Reset fields 413 this._chartTypeSeries = null; 414 this._funnelMinPointHeight = 0f; 415 416 // Save reference to the input parameters 417 this.Graph = graph; 418 this.Common = common; 419 this.Area = area; 420 421 // Funnel chart like a Pie chart shows each data point as part of the whole (100%). 422 // Calculate the sum of all Y and X values, which will be used to calculate point percentage. 423 GetDataPointValuesStatistic(); 424 425 // Check if there are non-zero points 426 if(this.yValueTotal == 0.0 || this.pointNumber == 0) 427 { 428 return; 429 } 430 431 // When Y value is funnel width at least 2 points required 432 this._funnelStyle = GetFunnelStyle( this.GetDataSeries() ); 433 if(this._funnelStyle == FunnelStyle.YIsWidth && 434 this.pointNumber == 1) 435 { 436 // At least 2 points required 437 return; 438 } 439 440 // Get minimum point height 441 GetFunnelMinPointHeight( this.GetDataSeries() ); 442 443 // Fill list of data point labels information 444 this.labelInfoList = CreateLabelsInfoList(); 445 446 // Calculate the spacing required for the labels. 447 GetPlotAreaSpacing(); 448 449 // Draw funnel 450 ProcessChartType(); 451 452 // Draw data point labels 453 DrawLabels(); 454 } 455 456 /// <summary> 457 /// Process chart type drawing. 458 /// </summary> ProcessChartType()459 private void ProcessChartType() 460 { 461 // Reversed drawing order in 3D with positive rotation angle 462 if(this.Area.Area3DStyle.Enable3D && 463 ( (this._rotation3D > 0 && !this.isPyramid) || (this._rotation3D < 0 && this.isPyramid) ) ) 464 { 465 this.segmentList.Reverse(); 466 } 467 468 // Check if series shadow should be drawn separatly 469 bool drawShadowSeparatly = true; 470 bool drawSegmentShadow = (this.Area.Area3DStyle.Enable3D) ? false : true; 471 472 // Process all funnel segments shadows 473 Series series = this.GetDataSeries(); 474 if(drawSegmentShadow && 475 drawShadowSeparatly && 476 series != null && 477 series.ShadowOffset != 0) 478 { 479 foreach(FunnelSegmentInfo segmentInfo in this.segmentList) 480 { 481 // Draw funnel segment 482 this.DrawFunnelCircularSegment( 483 segmentInfo.Point, 484 segmentInfo.PointIndex, 485 segmentInfo.StartWidth, 486 segmentInfo.EndWidth, 487 segmentInfo.Location, 488 segmentInfo.Height, 489 segmentInfo.NothingOnTop, 490 segmentInfo.NothingOnBottom, 491 false, 492 true); 493 } 494 495 drawSegmentShadow = false; 496 } 497 498 // Process all funnel segments 499 foreach(FunnelSegmentInfo segmentInfo in this.segmentList) 500 { 501 // Draw funnel segment 502 this.DrawFunnelCircularSegment( 503 segmentInfo.Point, 504 segmentInfo.PointIndex, 505 segmentInfo.StartWidth, 506 segmentInfo.EndWidth, 507 segmentInfo.Location, 508 segmentInfo.Height, 509 segmentInfo.NothingOnTop, 510 segmentInfo.NothingOnBottom, 511 true, 512 drawSegmentShadow); 513 } 514 } 515 516 /// <summary> 517 /// Gets funnel data point segment height and width. 518 /// </summary> 519 /// <param name="series">Chart type series.</param> 520 /// <param name="pointIndex">Data point index in the series.</param> 521 /// <param name="location">Segment top location. Bottom location if reversed drawing order.</param> 522 /// <param name="height">Returns the height of the segment.</param> 523 /// <param name="startWidth">Returns top width of the segment.</param> 524 /// <param name="endWidth">Returns botom width of the segment.</param> GetPointWidthAndHeight( Series series, int pointIndex, float location, out float height, out float startWidth, out float endWidth)525 protected virtual void GetPointWidthAndHeight( 526 Series series, 527 int pointIndex, 528 float location, 529 out float height, 530 out float startWidth, 531 out float endWidth) 532 { 533 PointF pointPositionAbs = PointF.Empty; 534 535 // Get plotting area position in pixels 536 RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(this.PlotAreaPosition); 537 538 // Calculate total height of plotting area minus reserved space for the gaps 539 float plotAreaHeightAbs = plotAreaPositionAbs.Height - 540 this.funnelSegmentGap * (this.pointNumber - ((ShouldDrawFirstPoint()) ? 1 : 2) ); 541 if(plotAreaHeightAbs < 0f) 542 { 543 plotAreaHeightAbs = 0f; 544 } 545 546 if( this._funnelStyle == FunnelStyle.YIsWidth ) 547 { 548 // Check if X values are provided 549 if(this._xValueTotal == 0.0) 550 { 551 // Calculate segment height in pixels by deviding 552 // plotting area height by number of points. 553 height = plotAreaHeightAbs / (this.pointNumber - 1); 554 } 555 else 556 { 557 // Calculate segment height as a part of total Y values in series 558 height = (float)(plotAreaHeightAbs * (GetXValue(series.Points[pointIndex]) / this._xValueTotal)); 559 } 560 561 // Check for minimum segment height 562 height = CheckMinHeight(height); 563 564 // Calculate start and end width of the segment based on Y value 565 // of previous and current data point. 566 startWidth = (float)(plotAreaPositionAbs.Width * (GetYValue(series.Points[pointIndex-1], pointIndex-1) / this._yValueMax)); 567 endWidth = (float)(plotAreaPositionAbs.Width * (GetYValue(series.Points[pointIndex], pointIndex) / this._yValueMax)); 568 569 // Set point position for annotation anchoring 570 pointPositionAbs = new PointF( 571 plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f, 572 location + height); 573 } 574 else if( this._funnelStyle == FunnelStyle.YIsHeight ) 575 { 576 // Calculate segment height as a part of total Y values in series 577 height = (float)(plotAreaHeightAbs * (GetYValue(series.Points[pointIndex], pointIndex) / this.yValueTotal)); 578 579 // Check for minimum segment height 580 height = CheckMinHeight(height); 581 582 // Get intersection point of the horizontal line at the start of the segment 583 // with the left pre-defined wall of the funnel. 584 PointF startIntersection = ChartGraphics.GetLinesIntersection( 585 plotAreaPositionAbs.X, location, 586 plotAreaPositionAbs.Right, location, 587 plotAreaPositionAbs.X, plotAreaPositionAbs.Y, 588 plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f - this._funnelNeckSize.Width / 2f, 589 plotAreaPositionAbs.Bottom - this._funnelNeckSize.Height ); 590 591 // Get intersection point of the horizontal line at the end of the segment 592 // with the left pre-defined wall of the funnel. 593 PointF endIntersection = ChartGraphics.GetLinesIntersection( 594 plotAreaPositionAbs.X, location + height, 595 plotAreaPositionAbs.Right, location + height, 596 plotAreaPositionAbs.X, plotAreaPositionAbs.Y, 597 plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f - this._funnelNeckSize.Width / 2f, 598 plotAreaPositionAbs.Bottom - this._funnelNeckSize.Height ); 599 600 // Get segment start and end width 601 startWidth = (float)( plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f - 602 startIntersection.X) * 2f; 603 endWidth = (float)( plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f - 604 endIntersection.X) * 2f; 605 606 // Set point position for annotation anchoring 607 pointPositionAbs = new PointF( 608 plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f, 609 location + height / 2f); 610 } 611 else 612 { 613 throw (new InvalidOperationException(SR.ExceptionFunnelStyleUnknown(this._funnelStyle.ToString()))); 614 } 615 616 // Set pre-calculated point position 617 series.Points[pointIndex].positionRel = Graph.GetRelativePoint(pointPositionAbs); 618 } 619 620 /// <summary> 621 /// Checks if first point in the series should be drawn. 622 /// When point Y value is used to define the diameter of the funnel 623 /// segment 2 points are required to draw 1 segment. In this case first 624 /// data point is not drawn. 625 /// </summary> 626 /// <returns>True if first point in the series should be drawn.</returns> ShouldDrawFirstPoint()627 protected virtual bool ShouldDrawFirstPoint() 628 { 629 return ( this._funnelStyle == FunnelStyle.YIsHeight || this.isPyramid); 630 } 631 632 /// <summary> 633 /// Draws funnel 3D square segment. 634 /// </summary> 635 /// <param name="point">Data point</param> 636 /// <param name="pointIndex">Data point index.</param> 637 /// <param name="startWidth">Segment top width.</param> 638 /// <param name="endWidth">Segment bottom width.</param> 639 /// <param name="location">Segment top location.</param> 640 /// <param name="height">Segment height.</param> 641 /// <param name="nothingOnTop">True if nothing is on the top of that segment.</param> 642 /// <param name="nothingOnBottom">True if nothing is on the bottom of that segment.</param> 643 /// <param name="drawSegment">True if segment shadow should be drawn.</param> 644 /// <param name="drawSegmentShadow">True if segment shadow should be drawn.</param> DrawFunnel3DSquareSegment( DataPoint point, int pointIndex, float startWidth, float endWidth, float location, float height, bool nothingOnTop, bool nothingOnBottom, bool drawSegment, bool drawSegmentShadow)645 private void DrawFunnel3DSquareSegment( 646 DataPoint point, 647 int pointIndex, 648 float startWidth, 649 float endWidth, 650 float location, 651 float height, 652 bool nothingOnTop, 653 bool nothingOnBottom, 654 bool drawSegment, 655 bool drawSegmentShadow) 656 { 657 // Increase the height of the segment to make sure there is no gaps between segments 658 if(!nothingOnBottom) 659 { 660 height += 0.3f; 661 } 662 663 // Get lighter and darker back colors 664 Color lightColor = ChartGraphics.GetGradientColor( point.Color, Color.White, 0.3 ); 665 Color darkColor = ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.3 ); 666 667 // Segment width can't be smaller than funnel neck width 668 if( this._funnelStyle == FunnelStyle.YIsHeight && !this.isPyramid ) 669 { 670 if(startWidth < this._funnelNeckSize.Width) 671 { 672 startWidth = this._funnelNeckSize.Width; 673 } 674 if(endWidth < this._funnelNeckSize.Width) 675 { 676 endWidth = this._funnelNeckSize.Width; 677 } 678 } 679 680 // Get 3D rotation angle 681 float topRotationHeight = (float)( (startWidth / 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) ); 682 float bottomRotationHeight = (float)( (endWidth / 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) ); 683 684 // Get plotting area position in pixels 685 RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(this.PlotAreaPosition); 686 687 // Get the horizontal center point in pixels 688 float xCenterPointAbs = plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f; 689 690 // Start Svg Selection mode 691 this.Graph.StartHotRegion( point ); 692 693 // Create segment path 694 GraphicsPath segmentPath = new GraphicsPath(); 695 696 // Draw left part of the pyramid segment 697 // Add top line 698 if(startWidth > 0f) 699 { 700 segmentPath.AddLine( 701 xCenterPointAbs - startWidth / 2f, location, 702 xCenterPointAbs, location + topRotationHeight); 703 } 704 705 // Add middle line 706 segmentPath.AddLine( 707 xCenterPointAbs, location + topRotationHeight, 708 xCenterPointAbs, location + height + bottomRotationHeight); 709 710 // Add bottom line 711 if(endWidth > 0f) 712 { 713 segmentPath.AddLine( 714 xCenterPointAbs, location + height + bottomRotationHeight, 715 xCenterPointAbs - endWidth / 2f, location + height); 716 } 717 718 // Add left line 719 segmentPath.AddLine( 720 xCenterPointAbs - endWidth / 2f, location + height, 721 xCenterPointAbs - startWidth / 2f, location); 722 723 if( this.Common.ProcessModePaint ) 724 { 725 // Fill graphics path 726 this.Graph.DrawPathAbs( 727 segmentPath, 728 (drawSegment) ? lightColor : Color.Transparent, 729 point.BackHatchStyle, 730 point.BackImage, 731 point.BackImageWrapMode, 732 point.BackImageTransparentColor, 733 point.BackImageAlignment, 734 point.BackGradientStyle, 735 (drawSegment) ? point.BackSecondaryColor : Color.Transparent, 736 (drawSegment) ? point.BorderColor : Color.Transparent, 737 point.BorderWidth, 738 point.BorderDashStyle, 739 PenAlignment.Center, 740 (drawSegmentShadow) ? point.series.ShadowOffset : 0, 741 point.series.ShadowColor); 742 } 743 744 if( this.Common.ProcessModeRegions ) 745 { 746 // Add hot region 747 this.Common.HotRegionsList.AddHotRegion( 748 segmentPath, 749 false, 750 this.Graph, 751 point, 752 point.series.Name, 753 pointIndex); 754 } 755 segmentPath.Dispose(); 756 757 758 759 // Draw right part of the pyramid segment 760 // Add top line 761 segmentPath = new GraphicsPath(); 762 if(startWidth > 0f) 763 { 764 segmentPath.AddLine( 765 xCenterPointAbs + startWidth / 2f, location, 766 xCenterPointAbs, location + topRotationHeight); 767 } 768 769 // Add middle line 770 segmentPath.AddLine( 771 xCenterPointAbs, location + topRotationHeight, 772 xCenterPointAbs, location + height + bottomRotationHeight); 773 774 // Add bottom line 775 if(endWidth > 0f) 776 { 777 segmentPath.AddLine( 778 xCenterPointAbs, location + height + bottomRotationHeight, 779 xCenterPointAbs + endWidth / 2f, location + height); 780 } 781 782 // Add right line 783 segmentPath.AddLine( 784 xCenterPointAbs + endWidth / 2f, location + height, 785 xCenterPointAbs + startWidth / 2f, location); 786 787 if( this.Common.ProcessModePaint ) 788 { 789 // Fill graphics path 790 this.Graph.DrawPathAbs( 791 segmentPath, 792 (drawSegment) ? darkColor : Color.Transparent, 793 point.BackHatchStyle, 794 point.BackImage, 795 point.BackImageWrapMode, 796 point.BackImageTransparentColor, 797 point.BackImageAlignment, 798 point.BackGradientStyle, 799 (drawSegment) ? point.BackSecondaryColor : Color.Transparent, 800 (drawSegment) ? point.BorderColor : Color.Transparent, 801 point.BorderWidth, 802 point.BorderDashStyle, 803 PenAlignment.Center, 804 (drawSegmentShadow) ? point.series.ShadowOffset : 0, 805 point.series.ShadowColor); 806 } 807 808 if( this.Common.ProcessModeRegions ) 809 { 810 // Add hot region 811 this.Common.HotRegionsList.AddHotRegion( 812 segmentPath, 813 false, 814 this.Graph, 815 point, 816 point.series.Name, 817 pointIndex); 818 } 819 segmentPath.Dispose(); 820 821 822 // Add top 3D surface 823 if(this._rotation3D > 0f && startWidth > 0f && nothingOnTop) 824 { 825 if(this.Area.Area3DStyle.Enable3D) 826 { 827 PointF[] sidePoints = new PointF[4]; 828 sidePoints[0] = new PointF(xCenterPointAbs + startWidth / 2f, location); 829 sidePoints[1] = new PointF(xCenterPointAbs, location + topRotationHeight); 830 sidePoints[2] = new PointF(xCenterPointAbs - startWidth / 2f, location); 831 sidePoints[3] = new PointF(xCenterPointAbs, location - topRotationHeight); 832 GraphicsPath topCurve = new GraphicsPath(); 833 topCurve.AddLines(sidePoints); 834 topCurve.CloseAllFigures(); 835 836 if( this.Common.ProcessModePaint ) 837 { 838 // Fill graphics path 839 this.Graph.DrawPathAbs( 840 topCurve, 841 (drawSegment) ? ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.4 ) : Color.Transparent, 842 point.BackHatchStyle, 843 point.BackImage, 844 point.BackImageWrapMode, 845 point.BackImageTransparentColor, 846 point.BackImageAlignment, 847 point.BackGradientStyle, 848 (drawSegment) ? point.BackSecondaryColor : Color.Transparent, 849 (drawSegment) ? point.BorderColor : Color.Transparent, 850 point.BorderWidth, 851 point.BorderDashStyle, 852 PenAlignment.Center, 853 (drawSegmentShadow) ? point.series.ShadowOffset : 0, 854 point.series.ShadowColor); 855 } 856 857 if( this.Common.ProcessModeRegions ) 858 { 859 // Add hot region 860 this.Common.HotRegionsList.AddHotRegion( 861 topCurve, 862 false, 863 this.Graph, 864 point, 865 point.series.Name, 866 pointIndex); 867 } 868 topCurve.Dispose(); 869 } 870 } 871 872 // Add bottom 3D surface 873 if(this._rotation3D < 0f && startWidth > 0f && nothingOnBottom) 874 { 875 if(this.Area.Area3DStyle.Enable3D) 876 { 877 PointF[] sidePoints = new PointF[4]; 878 sidePoints[0] = new PointF(xCenterPointAbs + endWidth / 2f, location + height); 879 sidePoints[1] = new PointF(xCenterPointAbs, location + height + bottomRotationHeight); 880 sidePoints[2] = new PointF(xCenterPointAbs - endWidth / 2f, location + height); 881 sidePoints[3] = new PointF(xCenterPointAbs, location + height - bottomRotationHeight); 882 GraphicsPath topCurve = new GraphicsPath(); 883 topCurve.AddLines(sidePoints); 884 topCurve.CloseAllFigures(); 885 886 if( this.Common.ProcessModePaint ) 887 { 888 // Fill graphics path 889 this.Graph.DrawPathAbs( 890 topCurve, 891 (drawSegment) ? ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.4 ) : Color.Transparent, 892 point.BackHatchStyle, 893 point.BackImage, 894 point.BackImageWrapMode, 895 point.BackImageTransparentColor, 896 point.BackImageAlignment, 897 point.BackGradientStyle, 898 (drawSegment) ? point.BackSecondaryColor : Color.Transparent, 899 (drawSegment) ? point.BorderColor : Color.Transparent, 900 point.BorderWidth, 901 point.BorderDashStyle, 902 PenAlignment.Center, 903 (drawSegmentShadow) ? point.series.ShadowOffset : 0, 904 point.series.ShadowColor); 905 } 906 907 if( this.Common.ProcessModeRegions ) 908 { 909 // Add hot region 910 this.Common.HotRegionsList.AddHotRegion( 911 topCurve, 912 false, 913 this.Graph, 914 point, 915 point.series.Name, 916 pointIndex); 917 } 918 topCurve.Dispose(); 919 920 } 921 } 922 923 // End Svg Selection mode 924 this.Graph.EndHotRegion( ); 925 } 926 927 /// <summary> 928 /// Draws funnel segment. 929 /// </summary> 930 /// <param name="point">Data point</param> 931 /// <param name="pointIndex">Data point index.</param> 932 /// <param name="startWidth">Segment top width.</param> 933 /// <param name="endWidth">Segment bottom width.</param> 934 /// <param name="location">Segment top location.</param> 935 /// <param name="height">Segment height.</param> 936 /// <param name="nothingOnTop">True if nothing is on the top of that segment.</param> 937 /// <param name="nothingOnBottom">True if nothing is on the bottom of that segment.</param> 938 /// <param name="drawSegment">True if segment shadow should be drawn.</param> 939 /// <param name="drawSegmentShadow">True if segment shadow should be drawn.</param> DrawFunnelCircularSegment( DataPoint point, int pointIndex, float startWidth, float endWidth, float location, float height, bool nothingOnTop, bool nothingOnBottom, bool drawSegment, bool drawSegmentShadow)940 private void DrawFunnelCircularSegment( 941 DataPoint point, 942 int pointIndex, 943 float startWidth, 944 float endWidth, 945 float location, 946 float height, 947 bool nothingOnTop, 948 bool nothingOnBottom, 949 bool drawSegment, 950 bool drawSegmentShadow) 951 { 952 PointF leftSideLinePoint = PointF.Empty; 953 PointF rightSideLinePoint = PointF.Empty; 954 955 // Check if square 3D segment should be drawn 956 if(this.Area.Area3DStyle.Enable3D && !round3DShape) 957 { 958 DrawFunnel3DSquareSegment( 959 point, 960 pointIndex, 961 startWidth, 962 endWidth, 963 location, 964 height, 965 nothingOnTop, 966 nothingOnBottom, 967 drawSegment, 968 drawSegmentShadow); 969 return; 970 } 971 972 // Increase the height of the segment to make sure there is no gaps between segments 973 if(!nothingOnBottom) 974 { 975 height += 0.3f; 976 } 977 978 // Segment width can't be smaller than funnel neck width 979 float originalStartWidth = startWidth; 980 float originalEndWidth = endWidth; 981 if( this._funnelStyle == FunnelStyle.YIsHeight && !this.isPyramid) 982 { 983 if(startWidth < this._funnelNeckSize.Width) 984 { 985 startWidth = this._funnelNeckSize.Width; 986 } 987 if(endWidth < this._funnelNeckSize.Width) 988 { 989 endWidth = this._funnelNeckSize.Width; 990 } 991 } 992 993 // Get 3D rotation angle 994 float tension = 0.8f; 995 float topRotationHeight = (float)( (startWidth / 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) ); 996 float bottomRotationHeight = (float)( (endWidth / 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) ); 997 998 // Get plotting area position in pixels 999 RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(this.PlotAreaPosition); 1000 1001 // Get the horizontal center point in pixels 1002 float xCenterPointAbs = plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f; 1003 1004 // Start Svg Selection mode 1005 this.Graph.StartHotRegion( point ); 1006 1007 // Create segment path 1008 GraphicsPath segmentPath = new GraphicsPath(); 1009 1010 // Add top line 1011 if(startWidth > 0f) 1012 { 1013 if(this.Area.Area3DStyle.Enable3D) 1014 { 1015 PointF[] sidePoints = new PointF[4]; 1016 sidePoints[0] = new PointF(xCenterPointAbs + startWidth / 2f, location); 1017 sidePoints[1] = new PointF(xCenterPointAbs, location + topRotationHeight); 1018 sidePoints[2] = new PointF(xCenterPointAbs - startWidth / 2f, location); 1019 sidePoints[3] = new PointF(xCenterPointAbs, location - topRotationHeight); 1020 GraphicsPath topCurve = new GraphicsPath(); 1021 topCurve.AddClosedCurve(sidePoints, tension); 1022 topCurve.Flatten(); 1023 topCurve.Reverse(); 1024 1025 Graph.AddEllipseSegment( 1026 segmentPath, 1027 topCurve, 1028 null, 1029 true, 1030 0f, 1031 out leftSideLinePoint, 1032 out rightSideLinePoint); 1033 } 1034 else 1035 { 1036 segmentPath.AddLine( 1037 xCenterPointAbs - startWidth / 2f, location, 1038 xCenterPointAbs + startWidth / 2f, location); 1039 } 1040 } 1041 1042 // Add right line 1043 if( this._funnelStyle == FunnelStyle.YIsHeight && 1044 !this.isPyramid && 1045 startWidth > this._funnelNeckSize.Width && 1046 endWidth <= this._funnelNeckSize.Width) 1047 { 1048 // Get intersection point of the vertical line at the neck border 1049 // with the left pre-defined wall of the funnel. 1050 PointF intersection = ChartGraphics.GetLinesIntersection( 1051 xCenterPointAbs + this._funnelNeckSize.Width / 2f, plotAreaPositionAbs.Top, 1052 xCenterPointAbs + this._funnelNeckSize.Width / 2f, plotAreaPositionAbs.Bottom, 1053 xCenterPointAbs + originalStartWidth / 2f, location, 1054 xCenterPointAbs + originalEndWidth / 2f, location + height); 1055 1056 // Adjust intersection point with top of the neck 1057 intersection.Y = plotAreaPositionAbs.Bottom - this._funnelNeckSize.Height; 1058 1059 // Add two segment line 1060 segmentPath.AddLine( 1061 xCenterPointAbs + startWidth / 2f, location, 1062 intersection.X, intersection.Y); 1063 segmentPath.AddLine( 1064 intersection.X, intersection.Y, 1065 intersection.X, location + height); 1066 } 1067 else 1068 { 1069 // Add straight line 1070 segmentPath.AddLine( 1071 xCenterPointAbs + startWidth / 2f, location, 1072 xCenterPointAbs + endWidth / 2f, location + height); 1073 } 1074 1075 // Add bottom line 1076 if(endWidth > 0f) 1077 { 1078 if(this.Area.Area3DStyle.Enable3D) 1079 { 1080 PointF[] sidePoints = new PointF[4]; 1081 sidePoints[0] = new PointF(xCenterPointAbs + endWidth / 2f, location + height); 1082 sidePoints[1] = new PointF(xCenterPointAbs, location + height + bottomRotationHeight); 1083 sidePoints[2] = new PointF(xCenterPointAbs - endWidth / 2f, location + height); 1084 sidePoints[3] = new PointF(xCenterPointAbs, location + height - bottomRotationHeight); 1085 GraphicsPath topCurve = new GraphicsPath(); 1086 topCurve.AddClosedCurve(sidePoints, tension); 1087 topCurve.Flatten(); 1088 topCurve.Reverse(); 1089 1090 using (GraphicsPath tmp = new GraphicsPath()) 1091 { 1092 Graph.AddEllipseSegment( 1093 tmp, 1094 topCurve, 1095 null, 1096 true, 1097 0f, 1098 out leftSideLinePoint, 1099 out rightSideLinePoint); 1100 1101 tmp.Reverse(); 1102 if (tmp.PointCount > 0) 1103 { 1104 segmentPath.AddPath(tmp, false); 1105 } 1106 } 1107 } 1108 else 1109 { 1110 segmentPath.AddLine( 1111 xCenterPointAbs + endWidth / 2f, location + height, 1112 xCenterPointAbs - endWidth / 2f, location + height); 1113 } 1114 } 1115 1116 // Add left line 1117 if( this._funnelStyle == FunnelStyle.YIsHeight && 1118 !this.isPyramid && 1119 startWidth > this._funnelNeckSize.Width && 1120 endWidth <= this._funnelNeckSize.Width) 1121 { 1122 // Get intersection point of the horizontal line at the start of the segment 1123 // with the left pre-defined wall of the funnel. 1124 PointF intersection = ChartGraphics.GetLinesIntersection( 1125 xCenterPointAbs - this._funnelNeckSize.Width / 2f, plotAreaPositionAbs.Top, 1126 xCenterPointAbs - this._funnelNeckSize.Width / 2f, plotAreaPositionAbs.Bottom, 1127 xCenterPointAbs - originalStartWidth / 2f, location, 1128 xCenterPointAbs - originalEndWidth / 2f, location + height); 1129 1130 // Adjust intersection point with top of the neck 1131 intersection.Y = plotAreaPositionAbs.Bottom - this._funnelNeckSize.Height; 1132 1133 // Add two segment line 1134 segmentPath.AddLine( 1135 intersection.X, location + height, 1136 intersection.X, intersection.Y); 1137 segmentPath.AddLine( 1138 intersection.X, intersection.Y, 1139 xCenterPointAbs - startWidth / 2f, location); 1140 } 1141 else 1142 { 1143 segmentPath.AddLine( 1144 xCenterPointAbs - endWidth / 2f, location + height, 1145 xCenterPointAbs - startWidth / 2f, location); 1146 } 1147 1148 if( this.Common.ProcessModePaint ) 1149 { 1150 // Draw lightStyle source blink effect in 3D 1151 if(this.Area.Area3DStyle.Enable3D && 1152 Graph.ActiveRenderingType == RenderingType.Gdi ) 1153 { 1154 // Get lighter and darker back colors 1155 Color lightColor = ChartGraphics.GetGradientColor( point.Color, Color.White, 0.3 ); 1156 Color darkColor = ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.3 ); 1157 1158 // Create linear gradient brush 1159 RectangleF boundsRect = segmentPath.GetBounds(); 1160 if(boundsRect.Width == 0f) 1161 { 1162 boundsRect.Width = 1f; 1163 } 1164 if(boundsRect.Height == 0f) 1165 { 1166 boundsRect.Height = 1f; 1167 } 1168 using( LinearGradientBrush brush = new LinearGradientBrush( 1169 boundsRect, 1170 lightColor, 1171 darkColor, 1172 0f) ) 1173 { 1174 // Set linear gradient brush interpolation colors 1175 ColorBlend colorBlend = new ColorBlend(5); 1176 colorBlend.Colors[0] = darkColor; 1177 colorBlend.Colors[1] = darkColor; 1178 colorBlend.Colors[2] = lightColor; 1179 colorBlend.Colors[3] = darkColor; 1180 colorBlend.Colors[4] = darkColor; 1181 1182 colorBlend.Positions[0] = 0.0f; 1183 colorBlend.Positions[1] = 0.0f; 1184 colorBlend.Positions[2] = 0.5f; 1185 colorBlend.Positions[3] = 1.0f; 1186 colorBlend.Positions[4] = 1.0f; 1187 1188 brush.InterpolationColors = colorBlend; 1189 1190 // Fill path 1191 this.Graph.Graphics.FillPath(brush, segmentPath); 1192 1193 // Draw path border 1194 Pen pen = new Pen(point.BorderColor, point.BorderWidth); 1195 pen.DashStyle = this.Graph.GetPenStyle( point.BorderDashStyle ); 1196 if(point.BorderWidth == 0 || 1197 point.BorderDashStyle == ChartDashStyle.NotSet || 1198 point.BorderColor == Color.Empty) 1199 { 1200 // Draw line of the darker color inside the cylinder 1201 pen = new Pen(ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.3 ), 1); 1202 pen.Alignment = PenAlignment.Inset; 1203 } 1204 1205 pen.StartCap = LineCap.Round; 1206 pen.EndCap = LineCap.Round; 1207 pen.LineJoin = LineJoin.Bevel; 1208 this.Graph.DrawPath(pen, segmentPath ); 1209 pen.Dispose(); 1210 } 1211 } 1212 else 1213 { 1214 // Fill graphics path 1215 this.Graph.DrawPathAbs( 1216 segmentPath, 1217 (drawSegment) ? point.Color : Color.Transparent, 1218 point.BackHatchStyle, 1219 point.BackImage, 1220 point.BackImageWrapMode, 1221 point.BackImageTransparentColor, 1222 point.BackImageAlignment, 1223 point.BackGradientStyle, 1224 (drawSegment) ? point.BackSecondaryColor : Color.Transparent, 1225 (drawSegment) ? point.BorderColor : Color.Transparent, 1226 point.BorderWidth, 1227 point.BorderDashStyle, 1228 PenAlignment.Center, 1229 (drawSegmentShadow) ? point.series.ShadowOffset : 0, 1230 point.series.ShadowColor); 1231 } 1232 } 1233 1234 if( this.Common.ProcessModeRegions ) 1235 { 1236 // Add hot region 1237 this.Common.HotRegionsList.AddHotRegion( 1238 segmentPath, 1239 false, 1240 this.Graph, 1241 point, 1242 point.series.Name, 1243 pointIndex); 1244 } 1245 segmentPath.Dispose(); 1246 1247 1248 // Add top 3D surface 1249 if(this._rotation3D > 0f && startWidth > 0f && nothingOnTop) 1250 { 1251 if(this.Area.Area3DStyle.Enable3D) 1252 { 1253 PointF[] sidePoints = new PointF[4]; 1254 sidePoints[0] = new PointF(xCenterPointAbs + startWidth / 2f, location); 1255 sidePoints[1] = new PointF(xCenterPointAbs, location + topRotationHeight); 1256 sidePoints[2] = new PointF(xCenterPointAbs - startWidth / 2f, location); 1257 sidePoints[3] = new PointF(xCenterPointAbs, location - topRotationHeight); 1258 GraphicsPath topCurve = new GraphicsPath(); 1259 topCurve.AddClosedCurve(sidePoints, tension); 1260 1261 if( this.Common.ProcessModePaint ) 1262 { 1263 // Fill graphics path 1264 this.Graph.DrawPathAbs( 1265 topCurve, 1266 (drawSegment) ? ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.4 ) : Color.Transparent, 1267 point.BackHatchStyle, 1268 point.BackImage, 1269 point.BackImageWrapMode, 1270 point.BackImageTransparentColor, 1271 point.BackImageAlignment, 1272 point.BackGradientStyle, 1273 (drawSegment) ? point.BackSecondaryColor : Color.Transparent, 1274 (drawSegment) ? point.BorderColor : Color.Transparent, 1275 point.BorderWidth, 1276 point.BorderDashStyle, 1277 PenAlignment.Center, 1278 (drawSegmentShadow) ? point.series.ShadowOffset : 0, 1279 point.series.ShadowColor); 1280 } 1281 1282 if( this.Common.ProcessModeRegions ) 1283 { 1284 // Add hot region 1285 this.Common.HotRegionsList.AddHotRegion( 1286 topCurve, 1287 false, 1288 this.Graph, 1289 point, 1290 point.series.Name, 1291 pointIndex); 1292 } 1293 topCurve.Dispose(); 1294 } 1295 } 1296 1297 // Add bottom 3D surface 1298 if(this._rotation3D < 0f && startWidth > 0f && nothingOnBottom) 1299 { 1300 if(this.Area.Area3DStyle.Enable3D) 1301 { 1302 PointF[] sidePoints = new PointF[4]; 1303 sidePoints[0] = new PointF(xCenterPointAbs + endWidth / 2f, location + height); 1304 sidePoints[1] = new PointF(xCenterPointAbs, location + height + bottomRotationHeight); 1305 sidePoints[2] = new PointF(xCenterPointAbs - endWidth / 2f, location + height); 1306 sidePoints[3] = new PointF(xCenterPointAbs, location + height - bottomRotationHeight); 1307 GraphicsPath topCurve = new GraphicsPath(); 1308 topCurve.AddClosedCurve(sidePoints, tension); 1309 1310 if( this.Common.ProcessModePaint ) 1311 { 1312 // Fill graphics path 1313 this.Graph.DrawPathAbs( 1314 topCurve, 1315 (drawSegment) ? ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.4 ) : Color.Transparent, 1316 point.BackHatchStyle, 1317 point.BackImage, 1318 point.BackImageWrapMode, 1319 point.BackImageTransparentColor, 1320 point.BackImageAlignment, 1321 point.BackGradientStyle, 1322 (drawSegment) ? point.BackSecondaryColor : Color.Transparent, 1323 (drawSegment) ? point.BorderColor : Color.Transparent, 1324 point.BorderWidth, 1325 point.BorderDashStyle, 1326 PenAlignment.Center, 1327 (drawSegmentShadow) ? point.series.ShadowOffset : 0, 1328 point.series.ShadowColor); 1329 } 1330 1331 if( this.Common.ProcessModeRegions ) 1332 { 1333 // Add hot region 1334 this.Common.HotRegionsList.AddHotRegion( 1335 topCurve, 1336 false, 1337 this.Graph, 1338 point, 1339 point.series.Name, 1340 pointIndex); 1341 } 1342 topCurve.Dispose(); 1343 1344 } 1345 } 1346 1347 // End Svg Selection mode 1348 this.Graph.EndHotRegion( ); 1349 } 1350 1351 1352 /// <summary> 1353 /// Fill list with information about every segment of the funnel. 1354 /// </summary> 1355 /// <returns>Funnel segment information list.</returns> GetFunnelSegmentPositions()1356 private ArrayList GetFunnelSegmentPositions() 1357 { 1358 // Create new list 1359 ArrayList list = new ArrayList(); 1360 1361 // Funnel chart process only first series in the chart area 1362 // and cannot be combined with any other chart types. 1363 Series series = GetDataSeries(); 1364 if( series != null ) 1365 { 1366 // Get funnel drawing style 1367 this._funnelStyle = GetFunnelStyle(series); 1368 1369 // Check if round or square base is used in 3D chart 1370 this.round3DShape = (GetFunnel3DDrawingStyle(series) == Funnel3DDrawingStyle.CircularBase); 1371 1372 // Get funnel points gap 1373 this.funnelSegmentGap = GetFunnelPointGap(series); 1374 1375 // Get funnel neck size 1376 this._funnelNeckSize = GetFunnelNeckSize(series); 1377 1378 // Loop through all ponts in the data series 1379 float currentLocation = this.Graph.GetAbsolutePoint(this.PlotAreaPosition.Location).Y; 1380 if(this.isPyramid) 1381 { 1382 // Pyramid is drawn in reversed order. 1383 currentLocation = this.Graph.GetAbsoluteRectangle(this.PlotAreaPosition).Bottom; 1384 } 1385 for( int pointIndex = 0; pointIndex >= 0 && pointIndex < series.Points.Count; pointIndex += 1 ) 1386 { 1387 DataPoint point = series.Points[pointIndex]; 1388 1389 // Check if first data point should be drawn 1390 if( pointIndex > 0 || ShouldDrawFirstPoint() ) 1391 { 1392 // Get height and width of each data point segment 1393 float startWidth = 0f; 1394 float endWidth = 0f; 1395 float height = 0f; 1396 GetPointWidthAndHeight( 1397 series, 1398 pointIndex, 1399 currentLocation, 1400 out height, 1401 out startWidth, 1402 out endWidth); 1403 1404 // Check visibility of previous and next points 1405 bool nothingOnTop = false; 1406 bool nothingOnBottom = false; 1407 if(this.funnelSegmentGap > 0) 1408 { 1409 nothingOnTop = true; 1410 nothingOnBottom = true; 1411 } 1412 else 1413 { 1414 if(ShouldDrawFirstPoint()) 1415 { 1416 if(pointIndex == 0 || 1417 series.Points[pointIndex-1].Color.A != 255) 1418 { 1419 if(this.isPyramid) 1420 { 1421 nothingOnBottom = true; 1422 } 1423 else 1424 { 1425 nothingOnTop = true; 1426 } 1427 } 1428 } 1429 else 1430 { 1431 if(pointIndex == 1 || 1432 series.Points[pointIndex-1].Color.A != 255) 1433 { 1434 if(this.isPyramid) 1435 { 1436 nothingOnBottom = true; 1437 } 1438 else 1439 { 1440 nothingOnTop = true; 1441 } 1442 } 1443 } 1444 if( pointIndex == series.Points.Count - 1) 1445 { 1446 if(this.isPyramid) 1447 { 1448 nothingOnTop = true; 1449 } 1450 else 1451 { 1452 nothingOnBottom = true; 1453 } 1454 } 1455 else if(series.Points[pointIndex+1].Color.A != 255) 1456 { 1457 if(this.isPyramid) 1458 { 1459 nothingOnTop = true; 1460 } 1461 else 1462 { 1463 nothingOnBottom = true; 1464 } 1465 } 1466 } 1467 1468 // Add segment information 1469 FunnelSegmentInfo info = new FunnelSegmentInfo(); 1470 info.Point = point; 1471 info.PointIndex = pointIndex; 1472 info.StartWidth = startWidth; 1473 info.EndWidth = endWidth; 1474 info.Location = (this.isPyramid) ? currentLocation - height : currentLocation; 1475 info.Height = height; 1476 info.NothingOnTop = nothingOnTop; 1477 info.NothingOnBottom = nothingOnBottom; 1478 list.Add(info); 1479 1480 // Increase current Y location 1481 if(this.isPyramid) 1482 { 1483 currentLocation -= height + this.funnelSegmentGap; 1484 } 1485 else 1486 { 1487 currentLocation += height + this.funnelSegmentGap; 1488 } 1489 } 1490 } 1491 } 1492 1493 return list; 1494 } 1495 1496 #endregion 1497 1498 #region Labels Methods 1499 1500 /// <summary> 1501 /// Draws funnel data point labels. 1502 /// </summary> DrawLabels()1503 private void DrawLabels() 1504 { 1505 // Loop through all labels 1506 foreach(FunnelPointLabelInfo labelInfo in this.labelInfoList) 1507 { 1508 if(!labelInfo.Position.IsEmpty && 1509 !float.IsNaN(labelInfo.Position.X) && 1510 !float.IsNaN(labelInfo.Position.Y) && 1511 !float.IsNaN(labelInfo.Position.Width) && 1512 !float.IsNaN(labelInfo.Position.Height) ) 1513 { 1514 // Start Svg Selection mode 1515 this.Graph.StartHotRegion( labelInfo.Point ); 1516 1517 // Get size of a single character used for spacing 1518 SizeF spacing = this.Graph.MeasureString( 1519 "W", 1520 labelInfo.Point.Font, 1521 new SizeF(1000f, 1000F), 1522 StringFormat.GenericTypographic ); 1523 1524 // Draw a callout line 1525 if( !labelInfo.CalloutPoint1.IsEmpty && 1526 !labelInfo.CalloutPoint2.IsEmpty && 1527 !float.IsNaN(labelInfo.CalloutPoint1.X) && 1528 !float.IsNaN(labelInfo.CalloutPoint1.Y) && 1529 !float.IsNaN(labelInfo.CalloutPoint2.X) && 1530 !float.IsNaN(labelInfo.CalloutPoint2.Y) ) 1531 { 1532 // Add spacing between text and callout line 1533 if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right) 1534 { 1535 labelInfo.CalloutPoint2.X -= spacing.Width / 2f; 1536 1537 // Add a small spacing between a callout line and a segment 1538 labelInfo.CalloutPoint1.X += 2; 1539 } 1540 else 1541 { 1542 labelInfo.CalloutPoint2.X += spacing.Width / 2f; 1543 1544 // Add a small spacing between a callout line and a segment 1545 labelInfo.CalloutPoint1.X += 2; 1546 } 1547 1548 // Get callout line color 1549 Color lineColor = GetCalloutLineColor(labelInfo.Point); 1550 1551 // Draw callout line 1552 this.Graph.DrawLineAbs( 1553 lineColor, 1554 1, 1555 ChartDashStyle.Solid, 1556 labelInfo.CalloutPoint1, 1557 labelInfo.CalloutPoint2 ); 1558 1559 } 1560 1561 // Get label background position 1562 RectangleF labelBackPosition = labelInfo.Position; 1563 labelBackPosition.Inflate(spacing.Width / 2f, spacing.Height / 8f); 1564 labelBackPosition = this.Graph.GetRelativeRectangle(labelBackPosition); 1565 1566 // Center label in the middle of the background rectangle 1567 using (StringFormat format = new StringFormat()) 1568 { 1569 format.Alignment = StringAlignment.Center; 1570 format.LineAlignment = StringAlignment.Center; 1571 1572 // Draw label text 1573 using (Brush brush = new SolidBrush(labelInfo.Point.LabelForeColor)) 1574 { 1575 1576 this.Graph.DrawPointLabelStringRel( 1577 this.Common, 1578 labelInfo.Text, 1579 labelInfo.Point.Font, 1580 brush, 1581 labelBackPosition, 1582 format, 1583 labelInfo.Point.LabelAngle, 1584 labelBackPosition, 1585 1586 labelInfo.Point.LabelBackColor, 1587 labelInfo.Point.LabelBorderColor, 1588 labelInfo.Point.LabelBorderWidth, 1589 labelInfo.Point.LabelBorderDashStyle, 1590 labelInfo.Point.series, 1591 labelInfo.Point, 1592 labelInfo.PointIndex); 1593 } 1594 1595 // End Svg Selection mode 1596 this.Graph.EndHotRegion(); 1597 } 1598 } 1599 } 1600 } 1601 1602 /// <summary> 1603 /// Creates a list of structures with the data point labels information. 1604 /// </summary> 1605 /// <returns>Array list of labels information.</returns> CreateLabelsInfoList()1606 private ArrayList CreateLabelsInfoList() 1607 { 1608 ArrayList list = new ArrayList(); 1609 1610 // Get area position in pixels 1611 RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle( this.Area.Position.ToRectangleF() ); 1612 1613 // Get funnel chart type series 1614 Series series = GetDataSeries(); 1615 if( series != null ) 1616 { 1617 // Loop through all ponts in the data series 1618 int pointIndex = 0; 1619 foreach( DataPoint point in series.Points ) 1620 { 1621 // Ignore empty points 1622 if( !point.IsEmpty ) 1623 { 1624 // Get some properties for performance 1625 string pointLabel = point.Label; 1626 bool pointShowLabelAsValue = point.IsValueShownAsLabel; 1627 1628 // Check if label text exists 1629 if(pointShowLabelAsValue || pointLabel.Length > 0) 1630 { 1631 // Create new point label information class 1632 FunnelPointLabelInfo labelInfo = new FunnelPointLabelInfo(); 1633 labelInfo.Point = point; 1634 labelInfo.PointIndex = pointIndex; 1635 1636 // Get point label text 1637 if( pointLabel.Length == 0 ) 1638 { 1639 labelInfo.Text = ValueConverter.FormatValue( 1640 point.series.Chart, 1641 point, 1642 point.Tag, 1643 point.YValues[0], 1644 point.LabelFormat, 1645 point.series.YValueType, 1646 ChartElementType.DataPoint); 1647 } 1648 else 1649 { 1650 labelInfo.Text = point.ReplaceKeywords(pointLabel); 1651 } 1652 1653 // Get label style 1654 labelInfo.Style = GetLabelStyle(point); 1655 1656 // Get inside label vertical alignment 1657 if(labelInfo.Style == FunnelLabelStyle.Inside) 1658 { 1659 labelInfo.VerticalAlignment = GetInsideLabelAlignment(point); 1660 } 1661 1662 // Get outside labels placement 1663 if(labelInfo.Style != FunnelLabelStyle.Inside) 1664 { 1665 labelInfo.OutsidePlacement = GetOutsideLabelPlacement(point); 1666 } 1667 1668 // Measure string size 1669 labelInfo.Size = this.Graph.MeasureString( 1670 labelInfo.Text, 1671 point.Font, 1672 plotAreaPositionAbs.Size, 1673 StringFormat.GenericTypographic); 1674 1675 // Add label information into the list 1676 if(labelInfo.Text.Length > 0 && 1677 labelInfo.Style != FunnelLabelStyle.Disabled) 1678 { 1679 list.Add(labelInfo); 1680 } 1681 } 1682 } 1683 ++pointIndex; 1684 } 1685 } 1686 return list; 1687 } 1688 1689 /// <summary> 1690 /// Changes required plotting area spacing, so that all labels fit. 1691 /// </summary> 1692 /// <returns>Return True if no resizing required.</returns> FitPointLabels()1693 private bool FitPointLabels() 1694 { 1695 // Convert plotting area position to pixels. 1696 // Make rectangle 4 pixels smaller on each side. 1697 RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(PlotAreaPosition); 1698 plotAreaPositionAbs.Inflate(-4f, -4f); 1699 1700 // Get position of each label 1701 GetLabelsPosition(); 1702 1703 // Get spacing required to draw labels 1704 RectangleF requiredSpacing = this.Graph.GetAbsoluteRectangle( new RectangleF(1f, 1f, 1f, 1f) ); 1705 foreach(FunnelPointLabelInfo labelInfo in this.labelInfoList) 1706 { 1707 // Add additional horizontal spacing for outside labels 1708 RectangleF position = labelInfo.Position; 1709 if(labelInfo.Style == FunnelLabelStyle.Outside || 1710 labelInfo.Style == FunnelLabelStyle.OutsideInColumn) 1711 { 1712 float spacing = 10f; 1713 if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right) 1714 { 1715 position.Width += spacing; 1716 } 1717 else if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Left) 1718 { 1719 position.X -= spacing; 1720 position.Width += spacing; 1721 } 1722 } 1723 1724 // Horizontal coordinates are ignored for Inside label style 1725 if(labelInfo.Style != FunnelLabelStyle.Inside) 1726 { 1727 if( (plotAreaPositionAbs.X - position.X) > requiredSpacing.X ) 1728 { 1729 requiredSpacing.X = plotAreaPositionAbs.X - position.X; 1730 } 1731 1732 if( (position.Right - plotAreaPositionAbs.Right) > requiredSpacing.Width ) 1733 { 1734 requiredSpacing.Width = position.Right - plotAreaPositionAbs.Right; 1735 } 1736 } 1737 1738 // Vertical spacing 1739 if( (plotAreaPositionAbs.Y - position.Y) > requiredSpacing.Y ) 1740 { 1741 requiredSpacing.Y = plotAreaPositionAbs.Y - position.Y; 1742 } 1743 1744 if( (position.Bottom - plotAreaPositionAbs.Bottom) > requiredSpacing.Height ) 1745 { 1746 requiredSpacing.Height = position.Bottom - plotAreaPositionAbs.Bottom; 1747 } 1748 } 1749 1750 // Convert spacing rectangle to relative coordinates 1751 requiredSpacing = this.Graph.GetRelativeRectangle(requiredSpacing); 1752 1753 // Check if non-default spacing was used 1754 if(requiredSpacing.X > 1f || 1755 requiredSpacing.Y > 1f || 1756 requiredSpacing.Width > 1f || 1757 requiredSpacing.Height > 1f ) 1758 { 1759 this.plotAreaSpacing = requiredSpacing; 1760 1761 // Get NEW plotting area position 1762 this.PlotAreaPosition = GetPlotAreaPosition(); 1763 1764 // Get NEW list of segments 1765 this.segmentList = GetFunnelSegmentPositions(); 1766 1767 // Get NEW position of each label 1768 GetLabelsPosition(); 1769 1770 return false; 1771 } 1772 1773 return true; 1774 } 1775 1776 /// <summary> 1777 /// Loops through the point labels list and calculates labels position 1778 /// based on their size, position and funnel chart shape. 1779 /// </summary> GetLabelsPosition()1780 private void GetLabelsPosition() 1781 { 1782 // Convert plotting area position to pixels 1783 RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(PlotAreaPosition); 1784 float plotAreaCenterXAbs = plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f; 1785 1786 // Define label spacing 1787 SizeF labelSpacing = new SizeF(3f, 3f); 1788 1789 //Loop through all labels 1790 foreach(FunnelPointLabelInfo labelInfo in this.labelInfoList) 1791 { 1792 // Get ----osiated funnel segment information 1793 bool lastLabel = false; 1794 int pointIndex = labelInfo.PointIndex + ((ShouldDrawFirstPoint()) ? 0 : 1); 1795 if(pointIndex > this.segmentList.Count && !ShouldDrawFirstPoint() ) 1796 { 1797 // Use last point index if first point is not drawn 1798 pointIndex = this.segmentList.Count; 1799 lastLabel = true; 1800 } 1801 FunnelSegmentInfo segmentInfo = null; 1802 foreach(FunnelSegmentInfo info in this.segmentList) 1803 { 1804 if(info.PointIndex == pointIndex) 1805 { 1806 segmentInfo = info; 1807 break; 1808 } 1809 } 1810 1811 // Check if segment was found 1812 if(segmentInfo != null) 1813 { 1814 // Set label width and height 1815 labelInfo.Position.Width = labelInfo.Size.Width; 1816 labelInfo.Position.Height = labelInfo.Size.Height; 1817 1818 //****************************************************** 1819 //** Labels are placed OUTSIDE of the funnel 1820 //****************************************************** 1821 if(labelInfo.Style == FunnelLabelStyle.Outside || 1822 labelInfo.Style == FunnelLabelStyle.OutsideInColumn) 1823 { 1824 // Define position 1825 if( this._funnelStyle == FunnelStyle.YIsHeight ) 1826 { 1827 // Get segment top and bottom diameter 1828 float topDiameter = segmentInfo.StartWidth; 1829 float bottomDiameter = segmentInfo.EndWidth; 1830 if(!this.isPyramid) 1831 { 1832 if(topDiameter < this._funnelNeckSize.Width) 1833 { 1834 topDiameter = this._funnelNeckSize.Width; 1835 } 1836 if(bottomDiameter < this._funnelNeckSize.Width) 1837 { 1838 bottomDiameter = this._funnelNeckSize.Width; 1839 } 1840 1841 // Adjust label position because segment is bent to make a neck 1842 if(segmentInfo.StartWidth >= this._funnelNeckSize.Width && 1843 segmentInfo.EndWidth < this._funnelNeckSize.Width) 1844 { 1845 bottomDiameter = segmentInfo.EndWidth; 1846 } 1847 } 1848 1849 // Get Y position 1850 labelInfo.Position.Y = (segmentInfo.Location + segmentInfo.Height / 2f) - 1851 labelInfo.Size.Height / 2f; 1852 1853 // Get X position 1854 if(labelInfo.Style == FunnelLabelStyle.OutsideInColumn) 1855 { 1856 if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right) 1857 { 1858 labelInfo.Position.X = plotAreaPositionAbs.Right + 1859 4f * labelSpacing.Width; 1860 1861 // Set callout line coordinates 1862 if(!this.isPyramid) 1863 { 1864 labelInfo.CalloutPoint1.X = plotAreaCenterXAbs + 1865 Math.Max(this._funnelNeckSize.Width/2f, (topDiameter + bottomDiameter) / 4f); 1866 } 1867 else 1868 { 1869 labelInfo.CalloutPoint1.X = plotAreaCenterXAbs + 1870 (topDiameter + bottomDiameter) / 4f; 1871 } 1872 labelInfo.CalloutPoint2.X = labelInfo.Position.X; 1873 } 1874 else 1875 { 1876 labelInfo.Position.X = plotAreaPositionAbs.X - 1877 labelInfo.Size.Width - 1878 4f * labelSpacing.Width; 1879 1880 // Set callout line coordinates 1881 if(!this.isPyramid) 1882 { 1883 labelInfo.CalloutPoint1.X = plotAreaCenterXAbs - 1884 Math.Max(this._funnelNeckSize.Width/2f, (topDiameter + bottomDiameter) / 4f); 1885 } 1886 else 1887 { 1888 labelInfo.CalloutPoint1.X = plotAreaCenterXAbs - 1889 (topDiameter + bottomDiameter) / 4f; 1890 } 1891 labelInfo.CalloutPoint2.X = labelInfo.Position.Right; 1892 } 1893 1894 // Fill rest of coordinates required for the callout line 1895 labelInfo.CalloutPoint1.Y = segmentInfo.Location + segmentInfo.Height / 2f; 1896 labelInfo.CalloutPoint2.Y = labelInfo.CalloutPoint1.Y; 1897 } 1898 else 1899 { 1900 if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right) 1901 { 1902 labelInfo.Position.X = plotAreaCenterXAbs + 1903 (topDiameter + bottomDiameter) / 4f + 1904 4f * labelSpacing.Width; 1905 } 1906 else 1907 { 1908 labelInfo.Position.X = plotAreaCenterXAbs - 1909 labelInfo.Size.Width - 1910 (topDiameter + bottomDiameter) / 4f - 1911 4f * labelSpacing.Width; 1912 } 1913 } 1914 } 1915 else 1916 { 1917 // Use bottom part of the segment for the last point 1918 if(lastLabel) 1919 { 1920 if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right) 1921 { 1922 labelInfo.Position.X = plotAreaCenterXAbs + 1923 segmentInfo.EndWidth / 2f + 1924 4f * labelSpacing.Width; 1925 } 1926 else 1927 { 1928 labelInfo.Position.X = plotAreaCenterXAbs - 1929 labelInfo.Size.Width - 1930 segmentInfo.EndWidth / 2f - 1931 4f * labelSpacing.Width; 1932 } 1933 labelInfo.Position.Y = segmentInfo.Location + 1934 segmentInfo.Height - 1935 labelInfo.Size.Height / 2f; 1936 } 1937 else 1938 { 1939 if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right) 1940 { 1941 labelInfo.Position.X = plotAreaCenterXAbs + 1942 segmentInfo.StartWidth / 2f + 1943 4f * labelSpacing.Width; 1944 } 1945 else 1946 { 1947 labelInfo.Position.X = plotAreaCenterXAbs - 1948 labelInfo.Size.Width - 1949 segmentInfo.StartWidth / 2f - 1950 4f * labelSpacing.Width; 1951 } 1952 labelInfo.Position.Y = segmentInfo.Location - 1953 labelInfo.Size.Height / 2f; 1954 } 1955 1956 if(labelInfo.Style == FunnelLabelStyle.OutsideInColumn) 1957 { 1958 if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right) 1959 { 1960 labelInfo.Position.X = plotAreaPositionAbs.Right + 1961 4f * labelSpacing.Width; 1962 1963 // Set callout line coordinates 1964 labelInfo.CalloutPoint1.X = plotAreaCenterXAbs + 1965 ( (lastLabel) ? segmentInfo.EndWidth : segmentInfo.StartWidth) / 2f; 1966 labelInfo.CalloutPoint2.X = labelInfo.Position.X; 1967 1968 } 1969 else 1970 { 1971 labelInfo.Position.X = plotAreaPositionAbs.X - 1972 labelInfo.Size.Width - 1973 4f * labelSpacing.Width; 1974 1975 // Set callout line coordinates 1976 labelInfo.CalloutPoint1.X = plotAreaCenterXAbs - 1977 ( (lastLabel) ? segmentInfo.EndWidth : segmentInfo.StartWidth) / 2f; 1978 labelInfo.CalloutPoint2.X = labelInfo.Position.Right; 1979 } 1980 1981 // Fill rest of coordinates required for the callout line 1982 labelInfo.CalloutPoint1.Y = segmentInfo.Location; 1983 if(lastLabel) 1984 { 1985 labelInfo.CalloutPoint1.Y += segmentInfo.Height; 1986 } 1987 labelInfo.CalloutPoint2.Y = labelInfo.CalloutPoint1.Y; 1988 1989 } 1990 } 1991 } 1992 1993 //****************************************************** 1994 //** Labels are placed INSIDE of the funnel 1995 //****************************************************** 1996 else if(labelInfo.Style == FunnelLabelStyle.Inside) 1997 { 1998 // Define position 1999 labelInfo.Position.X = plotAreaCenterXAbs - labelInfo.Size.Width / 2f; 2000 if( this._funnelStyle == FunnelStyle.YIsHeight ) 2001 { 2002 labelInfo.Position.Y = (segmentInfo.Location + segmentInfo.Height / 2f) - 2003 labelInfo.Size.Height / 2f; 2004 if(labelInfo.VerticalAlignment == FunnelLabelVerticalAlignment.Top) 2005 { 2006 labelInfo.Position.Y -= segmentInfo.Height / 2f - labelInfo.Size.Height / 2f - labelSpacing.Height; 2007 } 2008 else if(labelInfo.VerticalAlignment == FunnelLabelVerticalAlignment.Bottom) 2009 { 2010 labelInfo.Position.Y += segmentInfo.Height / 2f - labelInfo.Size.Height / 2f - labelSpacing.Height; 2011 } 2012 } 2013 else 2014 { 2015 labelInfo.Position.Y = segmentInfo.Location - labelInfo.Size.Height / 2f; 2016 if(labelInfo.VerticalAlignment == FunnelLabelVerticalAlignment.Top) 2017 { 2018 labelInfo.Position.Y -= labelInfo.Size.Height / 2f + labelSpacing.Height; 2019 } 2020 else if(labelInfo.VerticalAlignment == FunnelLabelVerticalAlignment.Bottom) 2021 { 2022 labelInfo.Position.Y += labelInfo.Size.Height / 2f + labelSpacing.Height; 2023 } 2024 2025 // Use bottom part of the segment for the last point 2026 if(lastLabel) 2027 { 2028 labelInfo.Position.Y += segmentInfo.Height; 2029 } 2030 } 2031 2032 // Adjust label Y position in 3D 2033 if(this.Area.Area3DStyle.Enable3D) 2034 { 2035 labelInfo.Position.Y += (float)( ( (segmentInfo.EndWidth + segmentInfo.StartWidth) / 4f) * Math.Sin(this._rotation3D / 180F * Math.PI) ); 2036 } 2037 } 2038 2039 //****************************************************** 2040 //** Check if label overlaps any previous label 2041 //****************************************************** 2042 int interation = 0; 2043 while( IsLabelsOverlap(labelInfo) && interation < 1000) 2044 { 2045 float shiftSize = (this.isPyramid) ? -3f : 3f; 2046 2047 // Move label down 2048 labelInfo.Position.Y += shiftSize; 2049 2050 // Move callout second point down 2051 if(!labelInfo.CalloutPoint2.IsEmpty) 2052 { 2053 labelInfo.CalloutPoint2.Y += shiftSize; 2054 } 2055 2056 ++interation; 2057 } 2058 2059 } 2060 } 2061 } 2062 2063 /// <summary> 2064 /// Checks if specified label overlaps any previous labels. 2065 /// </summary> 2066 /// <param name="testLabelInfo">Label to test.</param> 2067 /// <returns>True if labels overlapp.</returns> IsLabelsOverlap(FunnelPointLabelInfo testLabelInfo)2068 private bool IsLabelsOverlap(FunnelPointLabelInfo testLabelInfo) 2069 { 2070 // Increase rectangle size by 1 pixel 2071 RectangleF rect = testLabelInfo.Position; 2072 rect.Inflate(1f, 1f); 2073 2074 // Increase label rectangle if border is drawn around the label 2075 if(!testLabelInfo.Point.LabelBackColor.IsEmpty || 2076 (testLabelInfo.Point.LabelBorderWidth > 0 && 2077 !testLabelInfo.Point.LabelBorderColor.IsEmpty && 2078 testLabelInfo.Point.LabelBorderDashStyle != ChartDashStyle.NotSet) ) 2079 { 2080 rect.Inflate(4f, 4f); 2081 } 2082 2083 //Loop through all labels 2084 foreach(FunnelPointLabelInfo labelInfo in this.labelInfoList) 2085 { 2086 // Stop searching 2087 if(labelInfo.PointIndex == testLabelInfo.PointIndex) 2088 { 2089 break; 2090 } 2091 2092 // Check if label position overlaps 2093 if(!labelInfo.Position.IsEmpty && 2094 labelInfo.Position.IntersectsWith(rect) ) 2095 { 2096 return true; 2097 } 2098 } 2099 2100 return false; 2101 } 2102 2103 /// <summary> 2104 /// Gets label style of the data point. 2105 /// </summary> 2106 /// <returns>Label style of the data point.</returns> GetLabelStyle(DataPointCustomProperties properties)2107 private FunnelLabelStyle GetLabelStyle(DataPointCustomProperties properties) 2108 { 2109 // Set default label style 2110 FunnelLabelStyle labelStyle = FunnelLabelStyle.OutsideInColumn; 2111 2112 // Get string value of the custom attribute 2113 string attrValue = properties[this.funnelLabelStyleAttributeName]; 2114 if(attrValue != null && attrValue.Length > 0) 2115 { 2116 // Convert string to the labels style 2117 try 2118 { 2119 labelStyle = (FunnelLabelStyle)Enum.Parse(typeof(FunnelLabelStyle), attrValue, true); 2120 } 2121 catch 2122 { 2123 throw(new InvalidOperationException( SR.ExceptionCustomAttributeValueInvalid(labelStyle.ToString(), this.funnelLabelStyleAttributeName) ) ); 2124 } 2125 } 2126 return labelStyle; 2127 } 2128 2129 #endregion // Labels Methods 2130 2131 #region Position Methods 2132 2133 /// <summary> 2134 /// Calculate the spacing required for the labels. 2135 /// </summary> GetPlotAreaSpacing()2136 private void GetPlotAreaSpacing() 2137 { 2138 // Provide small spacing on the sides of chart area 2139 this.plotAreaSpacing = new RectangleF(1f, 1f, 1f, 1f); 2140 2141 // Get plotting area position 2142 this.PlotAreaPosition = GetPlotAreaPosition(); 2143 2144 // Get list of segments 2145 this.segmentList = GetFunnelSegmentPositions(); 2146 2147 // If plotting area position is automatic 2148 if( Area.InnerPlotPosition.Auto ) 2149 { 2150 // Set a position so that data labels fit 2151 // This method is called several time to adjust label position while 2152 // funnel side angle is changed 2153 int iteration = 0; 2154 while(!FitPointLabels() && iteration < 5) 2155 { 2156 iteration++; 2157 } 2158 } 2159 else 2160 { 2161 // Just get labels position 2162 GetLabelsPosition(); 2163 } 2164 2165 } 2166 2167 /// <summary> 2168 /// Gets a rectangle in relative coordinates where the funnel will chart 2169 /// will be drawn. 2170 /// </summary> 2171 /// <returns>Plotting are of the chart in relative coordinates.</returns> GetPlotAreaPosition()2172 private RectangleF GetPlotAreaPosition() 2173 { 2174 // Get plotting area rectangle position 2175 RectangleF plotAreaPosition = ( Area.InnerPlotPosition.Auto ) ? 2176 Area.Position.ToRectangleF() : Area.PlotAreaPosition.ToRectangleF(); 2177 2178 // NOTE: Fixes issue #4085 2179 // Do not allow decreasing of the plot area height more than 50% 2180 if(plotAreaSpacing.Y > plotAreaPosition.Height / 2f) 2181 { 2182 plotAreaSpacing.Y = plotAreaPosition.Height / 2f; 2183 } 2184 if(plotAreaSpacing.Height > plotAreaPosition.Height / 2f) 2185 { 2186 plotAreaSpacing.Height = plotAreaPosition.Height / 2f; 2187 } 2188 2189 // Decrease plotting are position using pre-calculated ratio 2190 plotAreaPosition.X += plotAreaSpacing.X; 2191 plotAreaPosition.Y += plotAreaSpacing.Y; 2192 plotAreaPosition.Width -= plotAreaSpacing.X + plotAreaSpacing.Width; 2193 plotAreaPosition.Height -= plotAreaSpacing.Y + plotAreaSpacing.Height; 2194 2195 // Apply vertical spacing on top and bottom to fit the 3D surfaces 2196 if(this.Area.Area3DStyle.Enable3D) 2197 { 2198 // Convert position to pixels 2199 RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(plotAreaPosition); 2200 2201 // Funnel chart process only first series in the chart area 2202 // and cannot be combined with any other chart types. 2203 Series series = GetDataSeries(); 2204 if( series != null ) 2205 { 2206 // Get 3D funnel rotation angle (from 10 to -10) 2207 this._rotation3D = GetFunnelRotation(series); 2208 } 2209 2210 // Get top and bottom spacing 2211 float topSpacing = (float)Math.Abs( (plotAreaPositionAbs.Width/ 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) ); 2212 float bottomSpacing = (float)Math.Abs( (plotAreaPositionAbs.Width/ 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) ); 2213 2214 // Adjust position 2215 if(this.isPyramid) 2216 { 2217 // Only bottom spacing for the pyramid 2218 plotAreaPositionAbs.Height -= bottomSpacing; 2219 } 2220 else 2221 { 2222 // Add top/bottom spacing 2223 plotAreaPositionAbs.Y += topSpacing; 2224 plotAreaPositionAbs.Height -= topSpacing + bottomSpacing; 2225 } 2226 2227 // Convert position back to relative coordinates 2228 plotAreaPosition = this.Graph.GetRelativeRectangle(plotAreaPositionAbs); 2229 } 2230 2231 return plotAreaPosition; 2232 } 2233 2234 #endregion // Position Methods 2235 2236 #region Helper Methods 2237 2238 /// <summary> 2239 /// Checks for minimum segment height. 2240 /// </summary> 2241 /// <param name="height">Current segment height.</param> 2242 /// <returns>Adjusted segment height.</returns> CheckMinHeight(float height)2243 protected float CheckMinHeight(float height) 2244 { 2245 // When point gap is used do not allow to have the segment heigth to be zero. 2246 float minSize = Math.Min(2f, this.funnelSegmentGap / 2f); 2247 if(this.funnelSegmentGap > 0 && 2248 height < minSize) 2249 { 2250 return minSize; 2251 } 2252 2253 return height; 2254 } 2255 2256 /// <summary> 2257 /// Gets minimum point height in pixels. 2258 /// </summary> 2259 /// <returns>Minimum point height in pixels.</returns> GetFunnelMinPointHeight(DataPointCustomProperties properties)2260 private void GetFunnelMinPointHeight(DataPointCustomProperties properties) 2261 { 2262 // Set default minimum point size 2263 this._funnelMinPointHeight = 0f; 2264 2265 // Get string value of the custom attribute 2266 string attrValue = properties[this.funnelPointMinHeight]; 2267 if (attrValue != null && attrValue.Length > 0) 2268 { 2269 // Convert string to the point gap size 2270 2271 float pointHeight; 2272 bool parseSucceed = float.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out pointHeight); 2273 if (parseSucceed) 2274 { 2275 this._funnelMinPointHeight = pointHeight; 2276 } 2277 2278 if (!parseSucceed || this._funnelMinPointHeight < 0f || this._funnelMinPointHeight > 100f) 2279 { 2280 throw (new InvalidOperationException(SR.ExceptionFunnelMinimumPointHeightAttributeInvalid)); 2281 } 2282 2283 // Check if specified value is too big 2284 this._funnelMinPointHeight = (float)(this.yValueTotal * this._funnelMinPointHeight / 100f); 2285 2286 // Get data statistic again using Min value 2287 GetDataPointValuesStatistic(); 2288 } 2289 2290 return; 2291 } 2292 2293 /// <summary> 2294 /// Gets 3D funnel rotation angle. 2295 /// </summary> 2296 /// <returns>Rotation angle.</returns> GetFunnelRotation(DataPointCustomProperties properties)2297 private int GetFunnelRotation(DataPointCustomProperties properties) 2298 { 2299 // Set default gap size 2300 int angle = 5; 2301 2302 // Get string value of the custom attribute 2303 string attrValue = properties[this.funnelRotationAngleAttributeName]; 2304 if (attrValue != null && attrValue.Length > 0) 2305 { 2306 // Convert string to the point gap size 2307 2308 int a; 2309 bool parseSucceed = int.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out a); 2310 if (parseSucceed) 2311 { 2312 angle = a; 2313 } 2314 2315 // Validate attribute value 2316 if (!parseSucceed || angle < -10 || angle > 10) 2317 { 2318 throw (new InvalidOperationException(SR.ExceptionFunnelAngleRangeInvalid)); 2319 } 2320 } 2321 2322 return angle; 2323 } 2324 2325 /// <summary> 2326 /// Gets callout line color. 2327 /// </summary> 2328 /// <returns>Callout line color.</returns> GetCalloutLineColor(DataPointCustomProperties properties)2329 private Color GetCalloutLineColor(DataPointCustomProperties properties) 2330 { 2331 // Set default gap size 2332 Color color = Color.Black; 2333 2334 // Get string value of the custom attribute 2335 string attrValue = properties[CustomPropertyName.CalloutLineColor]; 2336 if(attrValue != null && attrValue.Length > 0) 2337 { 2338 // Convert string to Color 2339 bool failed = false; 2340 ColorConverter colorConverter = new ColorConverter(); 2341 try 2342 { 2343 color = (Color)colorConverter.ConvertFromInvariantString(attrValue); 2344 } 2345 catch (ArgumentException) 2346 { 2347 failed = true; 2348 } 2349 catch (NotSupportedException) 2350 { 2351 failed = true; 2352 } 2353 2354 // In case of an error try to convert using local settings 2355 if(failed) 2356 { 2357 try 2358 { 2359 color = (Color)colorConverter.ConvertFromString(attrValue); 2360 } 2361 catch(ArgumentException) 2362 { 2363 throw(new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid( attrValue, "CalloutLineColor") ) ); 2364 } 2365 } 2366 2367 } 2368 2369 return color; 2370 } 2371 2372 /// <summary> 2373 /// Gets funnel neck size when shape of the funnel do not change. 2374 /// </summary> 2375 /// <returns>Funnel neck width and height.</returns> GetFunnelNeckSize(DataPointCustomProperties properties)2376 private SizeF GetFunnelNeckSize(DataPointCustomProperties properties) 2377 { 2378 // Set default gap size 2379 SizeF neckSize = new SizeF(5f, 5f); 2380 2381 // Get string value of the custom attribute 2382 string attrValue = properties[CustomPropertyName.FunnelNeckWidth]; 2383 if (attrValue != null && attrValue.Length > 0) 2384 { 2385 // Convert string to the point gap size 2386 2387 float w; 2388 bool parseSucceed = float.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out w); 2389 if (parseSucceed) 2390 { 2391 neckSize.Width = w; 2392 } 2393 2394 // Validate attribute value 2395 if (!parseSucceed || neckSize.Width < 0 || neckSize.Width > 100) 2396 { 2397 throw (new InvalidOperationException(SR.ExceptionFunnelNeckWidthInvalid)); 2398 } 2399 } 2400 2401 // Get string value of the custom attribute 2402 attrValue = properties[CustomPropertyName.FunnelNeckHeight]; 2403 if (attrValue != null && attrValue.Length > 0) 2404 { 2405 // Convert string to the point gap size 2406 float h; 2407 bool parseSucceed = float.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out h); 2408 if (parseSucceed) 2409 { 2410 neckSize.Height = h; 2411 } 2412 2413 2414 if (!parseSucceed || neckSize.Height < 0 || neckSize.Height > 100) 2415 { 2416 throw (new InvalidOperationException(SR.ExceptionFunnelNeckHeightInvalid)); 2417 } 2418 } 2419 2420 // Make sure the neck size do not exceed the plotting area size 2421 if(neckSize.Height > this.PlotAreaPosition.Height/2f) 2422 { 2423 neckSize.Height = this.PlotAreaPosition.Height/2f; 2424 } 2425 if(neckSize.Width > this.PlotAreaPosition.Width/2f) 2426 { 2427 neckSize.Width = this.PlotAreaPosition.Width/2f; 2428 } 2429 2430 // Convert from relative coordinates to pixels 2431 return this.Graph.GetAbsoluteSize(neckSize); 2432 } 2433 2434 /// <summary> 2435 /// Gets gap between points in pixels. 2436 /// </summary> 2437 /// <returns>Gap between funnel points.</returns> GetFunnelPointGap(DataPointCustomProperties properties)2438 private float GetFunnelPointGap(DataPointCustomProperties properties) 2439 { 2440 // Set default gap size 2441 float gapSize = 0f; 2442 2443 // Get string value of the custom attribute 2444 string attrValue = properties[this.funnelPointGapAttributeName]; 2445 if (attrValue != null && attrValue.Length > 0) 2446 { 2447 // Convert string to the point gap size 2448 2449 float gs; 2450 bool parseSucceed = float.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out gs); 2451 if (parseSucceed) 2452 { 2453 gapSize = gs; 2454 } 2455 else 2456 { 2457 throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid(attrValue, this.funnelPointGapAttributeName))); 2458 } 2459 2460 // Make sure the total gap size for all points do not exceed the total height of the plotting area 2461 float maxGapSize = this.PlotAreaPosition.Height / (this.pointNumber - ((ShouldDrawFirstPoint()) ? 1 : 2)); 2462 if (gapSize > maxGapSize) 2463 { 2464 gapSize = maxGapSize; 2465 } 2466 if (gapSize < 0) 2467 { 2468 gapSize = 0; 2469 } 2470 2471 // Convert from relative coordinates to pixels 2472 gapSize = this.Graph.GetAbsoluteSize(new SizeF(gapSize, gapSize)).Height; 2473 } 2474 2475 return gapSize; 2476 } 2477 2478 /// <summary> 2479 /// Gets funnel drawing style. 2480 /// </summary> 2481 /// <returns>funnel drawing style.</returns> GetFunnelStyle(DataPointCustomProperties properties)2482 private FunnelStyle GetFunnelStyle(DataPointCustomProperties properties) 2483 { 2484 // Set default funnel drawing style 2485 FunnelStyle drawingStyle = FunnelStyle.YIsHeight; 2486 2487 // Get string value of the custom attribute 2488 if(!this.isPyramid) 2489 { 2490 string attrValue = properties[CustomPropertyName.FunnelStyle]; 2491 if(attrValue != null && attrValue.Length > 0) 2492 { 2493 // Convert string to the labels style 2494 try 2495 { 2496 drawingStyle = (FunnelStyle)Enum.Parse(typeof(FunnelStyle), attrValue, true); 2497 } 2498 catch 2499 { 2500 throw(new InvalidOperationException( SR.ExceptionCustomAttributeValueInvalid( attrValue, "FunnelStyle") ) ); 2501 } 2502 } 2503 } 2504 return drawingStyle; 2505 } 2506 2507 /// <summary> 2508 /// Gets outside labels placement. 2509 /// </summary> 2510 /// <returns>Outside labels placement.</returns> GetOutsideLabelPlacement(DataPointCustomProperties properties)2511 private FunnelLabelPlacement GetOutsideLabelPlacement(DataPointCustomProperties properties) 2512 { 2513 // Set default vertical alignment for the inside labels 2514 FunnelLabelPlacement placement = FunnelLabelPlacement.Right; 2515 2516 // Get string value of the custom attribute 2517 string attrValue = properties[this.funnelOutsideLabelPlacementAttributeName]; 2518 if(attrValue != null && attrValue.Length > 0) 2519 { 2520 // Convert string to the labels placement 2521 try 2522 { 2523 placement = (FunnelLabelPlacement)Enum.Parse(typeof(FunnelLabelPlacement), attrValue, true); 2524 } 2525 catch 2526 { 2527 throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid(attrValue, this.funnelOutsideLabelPlacementAttributeName ))); 2528 } 2529 } 2530 return placement; 2531 } 2532 2533 /// <summary> 2534 /// Gets inside labels vertical alignment. 2535 /// </summary> 2536 /// <returns>Inside labels vertical alignment.</returns> GetInsideLabelAlignment(DataPointCustomProperties properties)2537 private FunnelLabelVerticalAlignment GetInsideLabelAlignment(DataPointCustomProperties properties) 2538 { 2539 // Set default vertical alignment for the inside labels 2540 FunnelLabelVerticalAlignment alignment = FunnelLabelVerticalAlignment.Center; 2541 2542 // Get string value of the custom attribute 2543 string attrValue = properties[this.funnelInsideLabelAlignmentAttributeName]; 2544 if(attrValue != null && attrValue.Length > 0) 2545 { 2546 // Convert string to the labels style 2547 try 2548 { 2549 alignment = (FunnelLabelVerticalAlignment)Enum.Parse(typeof(FunnelLabelVerticalAlignment), attrValue, true); 2550 } 2551 catch 2552 { 2553 throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid(attrValue, this.funnelInsideLabelAlignmentAttributeName))); 2554 } 2555 } 2556 return alignment; 2557 } 2558 2559 /// <summary> 2560 /// Gets funnel 3D drawing style. 2561 /// </summary> 2562 /// <returns>funnel drawing style.</returns> GetFunnel3DDrawingStyle(DataPointCustomProperties properties)2563 private Funnel3DDrawingStyle GetFunnel3DDrawingStyle(DataPointCustomProperties properties) 2564 { 2565 // Set default funnel drawing style 2566 Funnel3DDrawingStyle drawingStyle = (this.isPyramid) ? 2567 Funnel3DDrawingStyle.SquareBase : Funnel3DDrawingStyle.CircularBase; 2568 2569 // Get string value of the custom attribute 2570 string attrValue = properties[funnel3DDrawingStyleAttributeName]; 2571 if(attrValue != null && attrValue.Length > 0) 2572 { 2573 // Convert string to the labels style 2574 try 2575 { 2576 drawingStyle = (Funnel3DDrawingStyle)Enum.Parse(typeof(Funnel3DDrawingStyle), attrValue, true); 2577 } 2578 catch 2579 { 2580 throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid(attrValue, funnel3DDrawingStyleAttributeName) ) ); 2581 } 2582 } 2583 2584 return drawingStyle; 2585 } 2586 2587 /// <summary> 2588 /// Get data point Y and X values statistics: 2589 /// - Total of all Y values 2590 /// - Total of all X values 2591 /// - Maximum Y value 2592 /// Negative values are treated as positive. 2593 /// </summary> GetDataPointValuesStatistic()2594 private void GetDataPointValuesStatistic() 2595 { 2596 // Get funnel chart type series 2597 Series series = GetDataSeries(); 2598 if( series != null ) 2599 { 2600 // Reset values 2601 this.yValueTotal = 0.0; 2602 this._xValueTotal = 0.0; 2603 this._yValueMax = 0.0; 2604 this.pointNumber = 0; 2605 2606 // Get value type 2607 this._valuePercentages = null; 2608 PyramidValueType valueType = this.GetPyramidValueType( series ); 2609 if(valueType == PyramidValueType.Surface) 2610 { 2611 // Calculate the total surface area 2612 double triangleArea = 0.0; 2613 int pointIndex = 0; 2614 foreach( DataPoint point in series.Points ) 2615 { 2616 // Ignore empty points 2617 if( !point.IsEmpty ) 2618 { 2619 // Get Y value 2620 triangleArea += GetYValue(point, pointIndex); 2621 } 2622 ++pointIndex; 2623 } 2624 2625 // Calculate the base 2626 double triangleHeight = 100.0; 2627 double triangleBase = (2* triangleArea) / triangleHeight; 2628 2629 // Calculate the base to height ratio 2630 double baseRatio = triangleBase / triangleHeight; 2631 2632 // Calcuate the height percentage for each value 2633 double[] percentages = new double[series.Points.Count]; 2634 double sumArea = 0.0; 2635 for(int loop = 0; loop < percentages.Length; loop++) 2636 { 2637 double yValue = GetYValue(series.Points[loop], loop); 2638 sumArea += yValue; 2639 percentages[loop] = Math.Sqrt((2 * sumArea) / baseRatio); 2640 } 2641 this._valuePercentages = percentages; 2642 } 2643 2644 // Loop through all ponts in the data series 2645 foreach( DataPoint point in series.Points ) 2646 { 2647 // Ignore empty points 2648 if( !point.IsEmpty ) 2649 { 2650 // Get Y value 2651 double yValue = GetYValue(point, this.pointNumber); 2652 2653 // Get data point Y and X values statistics 2654 this.yValueTotal += yValue; 2655 this._yValueMax = Math.Max(this._yValueMax, yValue); 2656 this._xValueTotal += GetXValue(point); 2657 } 2658 2659 ++this.pointNumber; 2660 } 2661 2662 } 2663 } 2664 2665 /// <summary> 2666 /// Gets funnel chart series that belongs to the current chart area. 2667 /// Method also checks that only one visible Funnel series exists in the chart area. 2668 /// </summary> 2669 /// <returns>Funnel chart type series.</returns> GetDataSeries()2670 private Series GetDataSeries() 2671 { 2672 // Check if funnel series was already found 2673 if(this._chartTypeSeries == null) 2674 { 2675 // Loop through all series 2676 Series funnelSeries = null; 2677 foreach( Series series in Common.DataManager.Series ) 2678 { 2679 // Check if series is visible and belong to the current chart area 2680 if( series.IsVisible() && 2681 series.ChartArea == this.Area.Name ) 2682 { 2683 // Check series chart type is Funnel 2684 if( String.Compare( series.ChartTypeName, this.Name, true, System.Globalization.CultureInfo.CurrentCulture ) == 0 ) 2685 { 2686 if(funnelSeries == null) 2687 { 2688 funnelSeries = series; 2689 } 2690 } 2691 else if(!this.Common.ChartPicture.SuppressExceptions) 2692 { 2693 // Funnel chart can not be combined with other chart type 2694 throw (new InvalidOperationException(SR.ExceptionFunnelCanNotCombine)); 2695 } 2696 } 2697 } 2698 2699 // Remember the chart type series 2700 this._chartTypeSeries = funnelSeries; 2701 } 2702 2703 return this._chartTypeSeries; 2704 } 2705 2706 /// <summary> 2707 /// Gets pyramid value type. Each point value may represent a "Linear" height of 2708 /// the segment or "Surface" of the segment. 2709 /// </summary> 2710 /// <returns>Pyramid value type.</returns> GetPyramidValueType(DataPointCustomProperties properties)2711 private PyramidValueType GetPyramidValueType(DataPointCustomProperties properties) 2712 { 2713 // Set default funnel drawing style 2714 PyramidValueType valueType = PyramidValueType.Linear; 2715 2716 // Get string value of the custom attribute 2717 if(this.isPyramid) 2718 { 2719 string attrValue = properties[CustomPropertyName.PyramidValueType]; 2720 if(attrValue != null && attrValue.Length > 0) 2721 { 2722 // Convert string to the labels style 2723 try 2724 { 2725 valueType = (PyramidValueType)Enum.Parse(typeof(PyramidValueType), attrValue, true); 2726 } 2727 catch 2728 { 2729 throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid(attrValue,"PyramidValueType") ) ); 2730 } 2731 } 2732 } 2733 return valueType; 2734 } 2735 2736 #endregion // Helper Methods 2737 2738 #region Y & X values related methods 2739 2740 /// <summary> 2741 /// Helper function, which returns the Y value of the point. 2742 /// </summary> 2743 /// <param name="point">Point object.</param> 2744 /// <param name="pointIndex">Point index.</param> 2745 /// <returns>Y value of the point.</returns> GetYValue(DataPoint point, int pointIndex)2746 virtual public double GetYValue(DataPoint point, int pointIndex) 2747 { 2748 double yValue = 0.0; 2749 if( !point.IsEmpty ) 2750 { 2751 // Get Y value 2752 yValue = point.YValues[0]; 2753 2754 // Adjust point value 2755 if(this._valuePercentages != null && 2756 this._valuePercentages.Length > pointIndex ) 2757 { 2758 yValue = yValue / 100.0 * this._valuePercentages[pointIndex]; 2759 } 2760 2761 if(this.Area.AxisY.IsLogarithmic) 2762 { 2763 yValue = Math.Abs(Math.Log( yValue, this.Area.AxisY.LogarithmBase )); 2764 } 2765 else 2766 { 2767 yValue = Math.Abs( yValue ); 2768 if(yValue < this._funnelMinPointHeight) 2769 { 2770 yValue = this._funnelMinPointHeight; 2771 } 2772 } 2773 } 2774 return yValue; 2775 } 2776 2777 /// <summary> 2778 /// Helper function, which returns the X value of the point. 2779 /// </summary> 2780 /// <param name="point">Point object.</param> 2781 /// <returns>X value of the point.</returns> GetXValue(DataPoint point)2782 virtual public double GetXValue(DataPoint point) 2783 { 2784 if(this.Area.AxisX.IsLogarithmic) 2785 { 2786 return Math.Abs(Math.Log( point.XValue, this.Area.AxisX.LogarithmBase )); 2787 } 2788 return Math.Abs(point.XValue); 2789 } 2790 2791 /// <summary> 2792 /// Helper function, which returns the Y value of the point. 2793 /// </summary> 2794 /// <param name="common">Chart common elements.</param> 2795 /// <param name="area">Chart area the series belongs to.</param> 2796 /// <param name="series">Sereis of the point.</param> 2797 /// <param name="point">Point object.</param> 2798 /// <param name="pointIndex">Index of the point.</param> 2799 /// <param name="yValueIndex">Index of the Y value to get.</param> 2800 /// <returns>Y value of the point.</returns> GetYValue( CommonElements common, ChartArea area, Series series, DataPoint point, int pointIndex, int yValueIndex)2801 virtual public double GetYValue( 2802 CommonElements common, 2803 ChartArea area, 2804 Series series, 2805 DataPoint point, 2806 int pointIndex, 2807 int yValueIndex) 2808 { 2809 return point.YValues[yValueIndex]; 2810 } 2811 2812 #endregion // Y & X values related methods 2813 2814 #region SmartLabelStyle methods 2815 2816 /// <summary> 2817 /// Adds markers position to the list. Used to check SmartLabelStyle overlapping. 2818 /// </summary> 2819 /// <param name="common">Common chart elements.</param> 2820 /// <param name="area">Chart area.</param> 2821 /// <param name="series">Series values to be used.</param> 2822 /// <param name="list">List to add to.</param> AddSmartLabelMarkerPositions(CommonElements common, ChartArea area, Series series, ArrayList list)2823 public void AddSmartLabelMarkerPositions(CommonElements common, ChartArea area, Series series, ArrayList list) 2824 { 2825 // Fast Line chart type do not support labels 2826 } 2827 2828 #endregion 2829 2830 #region IDisposable interface implementation 2831 /// <summary> 2832 /// Releases unmanaged and - optionally - managed resources 2833 /// </summary> 2834 /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> Dispose(bool disposing)2835 protected virtual void Dispose(bool disposing) 2836 { 2837 //Nothing to dispose at the base class. 2838 } 2839 2840 /// <summary> 2841 /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 2842 /// </summary> Dispose()2843 public void Dispose() 2844 { 2845 Dispose(true); 2846 GC.SuppressFinalize(this); 2847 } 2848 #endregion 2849 } 2850 2851 /// <summary> 2852 /// PyramidChart class overrides some of the functionality of FunnelChart class. 2853 /// Most of drawing and othere processing is done in the FunnelChart. 2854 /// </summary> 2855 internal class PyramidChart : FunnelChart 2856 { 2857 #region Fields and Constructor 2858 2859 /// <summary> 2860 /// Default constructor 2861 /// </summary> PyramidChart()2862 public PyramidChart() 2863 { 2864 // Renering of the pyramid chart type 2865 base.isPyramid = true; 2866 2867 // Pyramid chart type uses square base by default 2868 base.round3DShape = false; 2869 2870 // Pyramid properties names 2871 base.funnelLabelStyleAttributeName = CustomPropertyName.PyramidLabelStyle; 2872 base.funnelPointGapAttributeName = CustomPropertyName.PyramidPointGap; 2873 base.funnelRotationAngleAttributeName = CustomPropertyName.Pyramid3DRotationAngle; 2874 base.funnelPointMinHeight = CustomPropertyName.PyramidMinPointHeight; 2875 base.funnel3DDrawingStyleAttributeName = CustomPropertyName.Pyramid3DDrawingStyle; 2876 base.funnelInsideLabelAlignmentAttributeName = CustomPropertyName.PyramidInsideLabelAlignment; 2877 base.funnelOutsideLabelPlacementAttributeName = CustomPropertyName.PyramidOutsideLabelPlacement; 2878 } 2879 2880 #endregion 2881 2882 #region IChartType interface implementation 2883 2884 /// <summary> 2885 /// Chart type name 2886 /// </summary> 2887 override public string Name { get{ return ChartTypeNames.Pyramid;}} 2888 2889 #endregion 2890 2891 #region Methods 2892 2893 /// <summary> 2894 /// Gets pyramid data point segment height and width. 2895 /// </summary> 2896 /// <param name="series">Chart type series.</param> 2897 /// <param name="pointIndex">Data point index in the series.</param> 2898 /// <param name="location">Segment top location. Bottom location if reversed drawing order.</param> 2899 /// <param name="height">Returns the height of the segment.</param> 2900 /// <param name="startWidth">Returns top width of the segment.</param> 2901 /// <param name="endWidth">Returns botom width of the segment.</param> GetPointWidthAndHeight( Series series, int pointIndex, float location, out float height, out float startWidth, out float endWidth)2902 protected override void GetPointWidthAndHeight( 2903 Series series, 2904 int pointIndex, 2905 float location, 2906 out float height, 2907 out float startWidth, 2908 out float endWidth) 2909 { 2910 PointF pointPositionAbs = PointF.Empty; 2911 2912 // Get plotting area position in pixels 2913 RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(this.PlotAreaPosition); 2914 2915 // Calculate total height of plotting area minus reserved space for the gaps 2916 float plotAreaHeightAbs = plotAreaPositionAbs.Height - 2917 this.funnelSegmentGap * (this.pointNumber - ((ShouldDrawFirstPoint()) ? 1 : 2) ); 2918 if(plotAreaHeightAbs < 0f) 2919 { 2920 plotAreaHeightAbs = 0f; 2921 } 2922 2923 // Calculate segment height as a part of total Y values in series 2924 height = (float)(plotAreaHeightAbs * (GetYValue(series.Points[pointIndex], pointIndex) / this.yValueTotal)); 2925 2926 // Check for minimum segment height 2927 height = CheckMinHeight(height); 2928 2929 // Get intersection point of the horizontal line at the start of the segment 2930 // with the left pre-defined wall of the funnel. 2931 PointF startIntersection = ChartGraphics.GetLinesIntersection( 2932 plotAreaPositionAbs.X, location - height, 2933 plotAreaPositionAbs.Right, location - height, 2934 plotAreaPositionAbs.X, plotAreaPositionAbs.Bottom, 2935 plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f, plotAreaPositionAbs.Y ); 2936 if(startIntersection.X > (plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f) ) 2937 { 2938 startIntersection.X = plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f; 2939 } 2940 2941 // Get intersection point of the horizontal line at the end of the segment 2942 // with the left pre-defined wall of the funnel. 2943 PointF endIntersection = ChartGraphics.GetLinesIntersection( 2944 plotAreaPositionAbs.X, location, 2945 plotAreaPositionAbs.Right, location, 2946 plotAreaPositionAbs.X, plotAreaPositionAbs.Bottom, 2947 plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f, plotAreaPositionAbs.Y ); 2948 if(endIntersection.X > (plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f)) 2949 { 2950 endIntersection.X = plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f; 2951 } 2952 2953 // Get segment start and end width 2954 startWidth = (float)Math.Abs( plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f - 2955 startIntersection.X) * 2f; 2956 endWidth = (float)Math.Abs( plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f - 2957 endIntersection.X) * 2f; 2958 2959 // Set point position for annotation anchoring 2960 pointPositionAbs = new PointF( 2961 plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f, 2962 location - height / 2f); 2963 2964 // Set pre-calculated point position 2965 series.Points[pointIndex].positionRel = Graph.GetRelativePoint(pointPositionAbs); 2966 } 2967 2968 #endregion // Methods 2969 } 2970 2971 /// <summary> 2972 /// Helper data structure used to store information about single funnel segment. 2973 /// </summary> 2974 internal class FunnelSegmentInfo 2975 { 2976 #region Fields 2977 2978 // ----osiated data point 2979 public DataPoint Point = null; 2980 2981 // Data point index 2982 public int PointIndex = 0; 2983 2984 // Segment top position 2985 public float Location = 0f; 2986 2987 // Segment height 2988 public float Height = 0f; 2989 2990 // Segment top width 2991 public float StartWidth = 0f; 2992 2993 // Segment bottom width 2994 public float EndWidth = 0f; 2995 2996 // Segment has nothing on the top 2997 public bool NothingOnTop = false; 2998 2999 // Segment has nothing on the bottom 3000 public bool NothingOnBottom = false; 3001 3002 #endregion // Fields 3003 } 3004 3005 /// <summary> 3006 /// Helper data structure used to store information about funnel data point label. 3007 /// </summary> 3008 internal class FunnelPointLabelInfo 3009 { 3010 #region Fields 3011 3012 // ----osiated data point 3013 public DataPoint Point = null; 3014 3015 // Data point index 3016 public int PointIndex = 0; 3017 3018 // Label text 3019 public string Text = string.Empty; 3020 3021 // Data point label size 3022 public SizeF Size = SizeF.Empty; 3023 3024 // Position of the data point label 3025 public RectangleF Position = RectangleF.Empty; 3026 3027 // Label style 3028 public FunnelLabelStyle Style = FunnelLabelStyle.OutsideInColumn; 3029 3030 // Inside label vertical alignment 3031 public FunnelLabelVerticalAlignment VerticalAlignment = FunnelLabelVerticalAlignment.Center; 3032 3033 // Outside labels placement 3034 public FunnelLabelPlacement OutsidePlacement = FunnelLabelPlacement.Right; 3035 3036 // Label callout first point 3037 public PointF CalloutPoint1 = PointF.Empty; 3038 3039 // Label callout second point 3040 public PointF CalloutPoint2 = PointF.Empty; 3041 3042 #endregion // Fields 3043 } 3044 } 3045