|
//-------------------------------------------------------------
// <copyright company=’Microsoft Corporation’>
// Copyright © Microsoft Corporation. All Rights Reserved.
// </copyright>
//-------------------------------------------------------------
// @owner=alexgor, deliant
//=================================================================
// File: FunnelChart.cs
//
// Namespace: DataVisualization.Charting.ChartTypes
//
// Classes: FunnelChart, PyramidChart, FunnelSegmentInfo,
// FunnelPointLabelInfo
//
// Purpose: Provides 2D/3D drawing and hit testing functionality
// for the Funnel and Pyramid charts.
//
// Funnel and Pyramid Chart types display data that
// equals 100% when totalled. This type of chart is a
// single series chart representing the data as portions
// of 100%, and this chart does not use any axes.
//
// Reviewed: AG - Microsoft 6, 2007
//
//===================================================================
#region Used namespaces
using System;
using System.Collections;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
#if Microsoft_CONTROL
using System.Windows.Forms.DataVisualization.Charting.Utilities;
#else
using System.Web.UI.DataVisualization.Charting.Utilities;
#endif
#endregion // Used namespaces
#if Microsoft_CONTROL
namespace System.Windows.Forms.DataVisualization.Charting.ChartTypes
#else // Microsoft_CONTROL
namespace System.Web.UI.DataVisualization.Charting.ChartTypes
#endif // Microsoft_CONTROL
{
#region Enumerations
/// <summary>
/// Value type of the pyramid chart.
/// </summary>
internal enum PyramidValueType
{
/// <summary>
/// Each point value defines linear height of each segment.
/// </summary>
Linear,
/// <summary>
/// Each point value defines surface of each segment.
/// </summary>
Surface
}
/// <summary>
/// Funnel chart drawing style.
/// </summary>
internal enum FunnelStyle
{
/// <summary>
/// Shape of the funnel is fixed and point Y value controls the height of the segments.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "YIs")]
YIsHeight,
/// <summary>
/// Height of each segment is the same and point Y value controls the diameter of the segment.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "YIs")]
YIsWidth
}
/// <summary>
/// Outside labels placement.
/// </summary>
internal enum FunnelLabelPlacement
{
/// <summary>
/// Labels are placed on the right side of the funnel.
/// </summary>
Right,
/// <summary>
/// Labels are placed on the left side of the funnel.
/// </summary>
Left
}
/// <summary>
/// Vertical alignment of the data point labels
/// </summary>
internal enum FunnelLabelVerticalAlignment
{
/// <summary>
/// Label placed in the middle.
/// </summary>
Center,
/// <summary>
/// Label placed on top.
/// </summary>
Top,
/// <summary>
/// Label placed on the bottom.
/// </summary>
Bottom
}
/// <summary>
/// Funnel chart 3D drawing style.
/// </summary>
internal enum Funnel3DDrawingStyle
{
/// <summary>
/// Circle will be used as a shape of the base.
/// </summary>
CircularBase,
/// <summary>
/// Square will be used as a shape of the base.
/// </summary>
SquareBase
}
/// <summary>
/// Funnel chart labels style enumeration.
/// </summary>
internal enum FunnelLabelStyle
{
/// <summary>
/// Data point labels are located inside of the funnel.
/// </summary>
Inside,
/// <summary>
/// Data point labels are located outside of the funnel.
/// </summary>
Outside,
/// <summary>
/// Data point labels are located outside of the funnel in a column.
/// </summary>
OutsideInColumn,
/// <summary>
/// Data point labels are disabled.
/// </summary>
Disabled
}
#endregion // Enumerations
/// <summary>
/// FunnelChart class provides 2D/3D drawing and hit testing functionality
/// for the Funnel and Pyramid charts.
/// </summary>
internal class FunnelChart : IChartType
{
#region Fields and Constructor
// Array list of funnel segments
internal ArrayList segmentList = null;
// List of data point labels information
internal ArrayList labelInfoList = null;
// Chart graphics object.
internal ChartGraphics Graph { get; set; }
// Chart area the chart type belongs to.
internal ChartArea Area { get; set; }
// Common chart elements.
internal CommonElements Common { get; set; }
// Spacing between each side of the funnel and chart area.
internal RectangleF plotAreaSpacing = new RectangleF(3f, 3f, 3f, 3f);
// Current chart type series
private Series _chartTypeSeries = null;
// Sum of all Y values in the data series
internal double yValueTotal = 0.0;
// Maximum Y value in the data series
private double _yValueMax = 0.0;
// Sum of all X values in the data series
private double _xValueTotal = 0.0;
// Number of points in the series
internal int pointNumber;
// Calculted plotting area of the chart
private RectangleF _plotAreaPosition = RectangleF.Empty;
// Funnel chart drawing style
private FunnelStyle _funnelStyle = FunnelStyle.YIsHeight;
// Define the shape of the funnel neck
private SizeF _funnelNeckSize = new SizeF(50f, 30f);
// Gap between funnel segments
internal float funnelSegmentGap = 0f;
// 3D funnel rotation angle
private int _rotation3D = 5;
// Indicates that rounded shape is used to draw 3D chart type instead of square
internal bool round3DShape = true;
// Indicates that Pyramid chart is rendered.
internal bool isPyramid = false;
// Minimum data point height
private float _funnelMinPointHeight = 0f;
// Name of the attribute that controls the height of the gap between the points
internal string funnelPointGapAttributeName = CustomPropertyName.FunnelPointGap;
// Name of the attribute that controls the 3D funnel rotation angle
internal string funnelRotationAngleAttributeName = CustomPropertyName.Funnel3DRotationAngle;
// Name of the attribute that controls the minimum height of the point
protected string funnelPointMinHeight = CustomPropertyName.FunnelMinPointHeight;
// Name of the attribute that controls the minimum height of the point
internal string funnel3DDrawingStyleAttributeName = CustomPropertyName.Funnel3DDrawingStyle;
// Name of the attribute that controls inside labels vertical alignment
internal string funnelInsideLabelAlignmentAttributeName = CustomPropertyName.FunnelInsideLabelAlignment;
// Name of the attribute that controls outside labels placement (Left vs. Right)
protected string funnelOutsideLabelPlacementAttributeName = CustomPropertyName.FunnelOutsideLabelPlacement;
// Name of the attribute that controls labels style
internal string funnelLabelStyleAttributeName = CustomPropertyName.FunnelLabelStyle;
// Array of data point value adjusments in percentage
private double[] _valuePercentages = null;
/// <summary>
/// Default constructor
/// </summary>
public FunnelChart()
{
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the calculted plotting area of the chart
/// </summary>
internal RectangleF PlotAreaPosition
{
get { return _plotAreaPosition; }
set { _plotAreaPosition = value; }
}
#endregion // Properties
#region IChartType interface implementation
/// <summary>
/// Chart type name
/// </summary>
virtual public string Name { get{ return ChartTypeNames.Funnel;}}
/// <summary>
/// True if chart type is stacked
/// </summary>
virtual public bool Stacked { get{ return false;}}
/// <summary>
/// True if stacked chart type supports groups
/// </summary>
virtual public bool SupportStackedGroups { get { return false; } }
/// <summary>
/// True if stacked chart type should draw separately positive and
/// negative data points ( Bar and column Stacked types ).
/// </summary>
public bool StackSign { get{ return false;}}
/// <summary>
/// True if chart type supports axeses
/// </summary>
virtual public bool RequireAxes { get{ return false;} }
/// <summary>
/// Chart type with two y values used for scale ( bubble chart type )
/// </summary>
virtual public bool SecondYScale{ get{ return false;} }
/// <summary>
/// True if chart type requires circular chart area.
/// </summary>
public bool CircularChartArea { get{ return false;} }
/// <summary>
/// True if chart type supports logarithmic axes
/// </summary>
virtual public bool SupportLogarithmicAxes { get{ return true;} }
/// <summary>
/// True if chart type requires to switch the value (Y) axes position
/// </summary>
virtual public bool SwitchValueAxes { get{ return false;} }
/// <summary>
/// True if chart series can be placed side-by-side.
/// </summary>
virtual public bool SideBySideSeries { get{ return false;} }
/// <summary>
/// True if each data point of a chart must be represented in the legend
/// </summary>
virtual public bool DataPointsInLegend { get{ return true;} }
/// <summary>
/// If the crossing value is auto Crossing value should be
/// automatically set to zero for some chart
/// types (Bar, column, area etc.)
/// </summary>
virtual public bool ZeroCrossing { get{ return false;} }
/// <summary>
/// True if palette colors should be applied for each data paoint.
/// Otherwise the color is applied to the series.
/// </summary>
virtual public bool ApplyPaletteColorsToPoints { get { return true; } }
/// <summary>
/// Indicates that extra Y values are connected to the scale of the Y axis
/// </summary>
virtual public bool ExtraYValuesConnectedToYAxis{ get { return false; } }
/// <summary>
/// Indicates that it's a hundredred percent chart.
/// Axis scale from 0 to 100 percent should be used.
/// </summary>
virtual public bool HundredPercent{ get{return false;} }
/// <summary>
/// Indicates that it's a hundredred percent chart.
/// Axis scale from 0 to 100 percent should be used.
/// </summary>
virtual public bool HundredPercentSupportNegative{ get{return false;} }
/// <summary>
/// How to draw series/points in legend:
/// Filled rectangle, Line or Marker
/// </summary>
/// <param name="series">Legend item series.</param>
/// <returns>Legend item style.</returns>
virtual public LegendImageStyle GetLegendImageStyle(Series series)
{
return LegendImageStyle.Rectangle;
}
/// <summary>
/// Number of supported Y value(s) per point
/// </summary>
virtual public int YValuesPerPoint { get { return 1; } }
/// <summary>
/// Gets chart type image.
/// </summary>
/// <param name="registry">Chart types registry object.</param>
/// <returns>Chart type image.</returns>
virtual public System.Drawing.Image GetImage(ChartTypeRegistry registry)
{
return (System.Drawing.Image)registry.ResourceManager.GetObject(this.Name + "ChartType");
}
#endregion
#region Painting
/// <summary>
/// Paint Funnel Chart.
/// </summary>
/// <param name="graph">The Chart Graphics object.</param>
/// <param name="common">The Common elements object.</param>
/// <param name="area">Chart area for this chart.</param>
/// <param name="seriesToDraw">Chart series to draw.</param>
virtual public void Paint(
ChartGraphics graph,
CommonElements common,
ChartArea area,
Series seriesToDraw )
{
// Reset fields
this._chartTypeSeries = null;
this._funnelMinPointHeight = 0f;
// Save reference to the input parameters
this.Graph = graph;
this.Common = common;
this.Area = area;
// Funnel chart like a Pie chart shows each data point as part of the whole (100%).
// Calculate the sum of all Y and X values, which will be used to calculate point percentage.
GetDataPointValuesStatistic();
// Check if there are non-zero points
if(this.yValueTotal == 0.0 || this.pointNumber == 0)
{
return;
}
// When Y value is funnel width at least 2 points required
this._funnelStyle = GetFunnelStyle( this.GetDataSeries() );
if(this._funnelStyle == FunnelStyle.YIsWidth &&
this.pointNumber == 1)
{
// At least 2 points required
return;
}
// Get minimum point height
GetFunnelMinPointHeight( this.GetDataSeries() );
// Fill list of data point labels information
this.labelInfoList = CreateLabelsInfoList();
// Calculate the spacing required for the labels.
GetPlotAreaSpacing();
// Draw funnel
ProcessChartType();
// Draw data point labels
DrawLabels();
}
/// <summary>
/// Process chart type drawing.
/// </summary>
private void ProcessChartType()
{
// Reversed drawing order in 3D with positive rotation angle
if(this.Area.Area3DStyle.Enable3D &&
( (this._rotation3D > 0 && !this.isPyramid) || (this._rotation3D < 0 && this.isPyramid) ) )
{
this.segmentList.Reverse();
}
// Check if series shadow should be drawn separatly
bool drawShadowSeparatly = true;
bool drawSegmentShadow = (this.Area.Area3DStyle.Enable3D) ? false : true;
// Process all funnel segments shadows
Series series = this.GetDataSeries();
if(drawSegmentShadow &&
drawShadowSeparatly &&
series != null &&
series.ShadowOffset != 0)
{
foreach(FunnelSegmentInfo segmentInfo in this.segmentList)
{
// Draw funnel segment
this.DrawFunnelCircularSegment(
segmentInfo.Point,
segmentInfo.PointIndex,
segmentInfo.StartWidth,
segmentInfo.EndWidth,
segmentInfo.Location,
segmentInfo.Height,
segmentInfo.NothingOnTop,
segmentInfo.NothingOnBottom,
false,
true);
}
drawSegmentShadow = false;
}
// Process all funnel segments
foreach(FunnelSegmentInfo segmentInfo in this.segmentList)
{
// Draw funnel segment
this.DrawFunnelCircularSegment(
segmentInfo.Point,
segmentInfo.PointIndex,
segmentInfo.StartWidth,
segmentInfo.EndWidth,
segmentInfo.Location,
segmentInfo.Height,
segmentInfo.NothingOnTop,
segmentInfo.NothingOnBottom,
true,
drawSegmentShadow);
}
}
/// <summary>
/// Gets funnel data point segment height and width.
/// </summary>
/// <param name="series">Chart type series.</param>
/// <param name="pointIndex">Data point index in the series.</param>
/// <param name="location">Segment top location. Bottom location if reversed drawing order.</param>
/// <param name="height">Returns the height of the segment.</param>
/// <param name="startWidth">Returns top width of the segment.</param>
/// <param name="endWidth">Returns botom width of the segment.</param>
protected virtual void GetPointWidthAndHeight(
Series series,
int pointIndex,
float location,
out float height,
out float startWidth,
out float endWidth)
{
PointF pointPositionAbs = PointF.Empty;
// Get plotting area position in pixels
RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(this.PlotAreaPosition);
// Calculate total height of plotting area minus reserved space for the gaps
float plotAreaHeightAbs = plotAreaPositionAbs.Height -
this.funnelSegmentGap * (this.pointNumber - ((ShouldDrawFirstPoint()) ? 1 : 2) );
if(plotAreaHeightAbs < 0f)
{
plotAreaHeightAbs = 0f;
}
if( this._funnelStyle == FunnelStyle.YIsWidth )
{
// Check if X values are provided
if(this._xValueTotal == 0.0)
{
// Calculate segment height in pixels by deviding
// plotting area height by number of points.
height = plotAreaHeightAbs / (this.pointNumber - 1);
}
else
{
// Calculate segment height as a part of total Y values in series
height = (float)(plotAreaHeightAbs * (GetXValue(series.Points[pointIndex]) / this._xValueTotal));
}
// Check for minimum segment height
height = CheckMinHeight(height);
// Calculate start and end width of the segment based on Y value
// of previous and current data point.
startWidth = (float)(plotAreaPositionAbs.Width * (GetYValue(series.Points[pointIndex-1], pointIndex-1) / this._yValueMax));
endWidth = (float)(plotAreaPositionAbs.Width * (GetYValue(series.Points[pointIndex], pointIndex) / this._yValueMax));
// Set point position for annotation anchoring
pointPositionAbs = new PointF(
plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f,
location + height);
}
else if( this._funnelStyle == FunnelStyle.YIsHeight )
{
// Calculate segment height as a part of total Y values in series
height = (float)(plotAreaHeightAbs * (GetYValue(series.Points[pointIndex], pointIndex) / this.yValueTotal));
// Check for minimum segment height
height = CheckMinHeight(height);
// Get intersection point of the horizontal line at the start of the segment
// with the left pre-defined wall of the funnel.
PointF startIntersection = ChartGraphics.GetLinesIntersection(
plotAreaPositionAbs.X, location,
plotAreaPositionAbs.Right, location,
plotAreaPositionAbs.X, plotAreaPositionAbs.Y,
plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f - this._funnelNeckSize.Width / 2f,
plotAreaPositionAbs.Bottom - this._funnelNeckSize.Height );
// Get intersection point of the horizontal line at the end of the segment
// with the left pre-defined wall of the funnel.
PointF endIntersection = ChartGraphics.GetLinesIntersection(
plotAreaPositionAbs.X, location + height,
plotAreaPositionAbs.Right, location + height,
plotAreaPositionAbs.X, plotAreaPositionAbs.Y,
plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f - this._funnelNeckSize.Width / 2f,
plotAreaPositionAbs.Bottom - this._funnelNeckSize.Height );
// Get segment start and end width
startWidth = (float)( plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f -
startIntersection.X) * 2f;
endWidth = (float)( plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f -
endIntersection.X) * 2f;
// Set point position for annotation anchoring
pointPositionAbs = new PointF(
plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f,
location + height / 2f);
}
else
{
throw (new InvalidOperationException(SR.ExceptionFunnelStyleUnknown(this._funnelStyle.ToString())));
}
// Set pre-calculated point position
series.Points[pointIndex].positionRel = Graph.GetRelativePoint(pointPositionAbs);
}
/// <summary>
/// Checks if first point in the series should be drawn.
/// When point Y value is used to define the diameter of the funnel
/// segment 2 points are required to draw 1 segment. In this case first
/// data point is not drawn.
/// </summary>
/// <returns>True if first point in the series should be drawn.</returns>
protected virtual bool ShouldDrawFirstPoint()
{
return ( this._funnelStyle == FunnelStyle.YIsHeight || this.isPyramid);
}
/// <summary>
/// Draws funnel 3D square segment.
/// </summary>
/// <param name="point">Data point</param>
/// <param name="pointIndex">Data point index.</param>
/// <param name="startWidth">Segment top width.</param>
/// <param name="endWidth">Segment bottom width.</param>
/// <param name="location">Segment top location.</param>
/// <param name="height">Segment height.</param>
/// <param name="nothingOnTop">True if nothing is on the top of that segment.</param>
/// <param name="nothingOnBottom">True if nothing is on the bottom of that segment.</param>
/// <param name="drawSegment">True if segment shadow should be drawn.</param>
/// <param name="drawSegmentShadow">True if segment shadow should be drawn.</param>
private void DrawFunnel3DSquareSegment(
DataPoint point,
int pointIndex,
float startWidth,
float endWidth,
float location,
float height,
bool nothingOnTop,
bool nothingOnBottom,
bool drawSegment,
bool drawSegmentShadow)
{
// Increase the height of the segment to make sure there is no gaps between segments
if(!nothingOnBottom)
{
height += 0.3f;
}
// Get lighter and darker back colors
Color lightColor = ChartGraphics.GetGradientColor( point.Color, Color.White, 0.3 );
Color darkColor = ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.3 );
// Segment width can't be smaller than funnel neck width
if( this._funnelStyle == FunnelStyle.YIsHeight && !this.isPyramid )
{
if(startWidth < this._funnelNeckSize.Width)
{
startWidth = this._funnelNeckSize.Width;
}
if(endWidth < this._funnelNeckSize.Width)
{
endWidth = this._funnelNeckSize.Width;
}
}
// Get 3D rotation angle
float topRotationHeight = (float)( (startWidth / 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) );
float bottomRotationHeight = (float)( (endWidth / 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) );
// Get plotting area position in pixels
RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(this.PlotAreaPosition);
// Get the horizontal center point in pixels
float xCenterPointAbs = plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f;
// Start Svg Selection mode
this.Graph.StartHotRegion( point );
// Create segment path
GraphicsPath segmentPath = new GraphicsPath();
// Draw left part of the pyramid segment
// Add top line
if(startWidth > 0f)
{
segmentPath.AddLine(
xCenterPointAbs - startWidth / 2f, location,
xCenterPointAbs, location + topRotationHeight);
}
// Add middle line
segmentPath.AddLine(
xCenterPointAbs, location + topRotationHeight,
xCenterPointAbs, location + height + bottomRotationHeight);
// Add bottom line
if(endWidth > 0f)
{
segmentPath.AddLine(
xCenterPointAbs, location + height + bottomRotationHeight,
xCenterPointAbs - endWidth / 2f, location + height);
}
// Add left line
segmentPath.AddLine(
xCenterPointAbs - endWidth / 2f, location + height,
xCenterPointAbs - startWidth / 2f, location);
if( this.Common.ProcessModePaint )
{
// Fill graphics path
this.Graph.DrawPathAbs(
segmentPath,
(drawSegment) ? lightColor : Color.Transparent,
point.BackHatchStyle,
point.BackImage,
point.BackImageWrapMode,
point.BackImageTransparentColor,
point.BackImageAlignment,
point.BackGradientStyle,
(drawSegment) ? point.BackSecondaryColor : Color.Transparent,
(drawSegment) ? point.BorderColor : Color.Transparent,
point.BorderWidth,
point.BorderDashStyle,
PenAlignment.Center,
(drawSegmentShadow) ? point.series.ShadowOffset : 0,
point.series.ShadowColor);
}
if( this.Common.ProcessModeRegions )
{
// Add hot region
this.Common.HotRegionsList.AddHotRegion(
segmentPath,
false,
this.Graph,
point,
point.series.Name,
pointIndex);
}
segmentPath.Dispose();
// Draw right part of the pyramid segment
// Add top line
segmentPath = new GraphicsPath();
if(startWidth > 0f)
{
segmentPath.AddLine(
xCenterPointAbs + startWidth / 2f, location,
xCenterPointAbs, location + topRotationHeight);
}
// Add middle line
segmentPath.AddLine(
xCenterPointAbs, location + topRotationHeight,
xCenterPointAbs, location + height + bottomRotationHeight);
// Add bottom line
if(endWidth > 0f)
{
segmentPath.AddLine(
xCenterPointAbs, location + height + bottomRotationHeight,
xCenterPointAbs + endWidth / 2f, location + height);
}
// Add right line
segmentPath.AddLine(
xCenterPointAbs + endWidth / 2f, location + height,
xCenterPointAbs + startWidth / 2f, location);
if( this.Common.ProcessModePaint )
{
// Fill graphics path
this.Graph.DrawPathAbs(
segmentPath,
(drawSegment) ? darkColor : Color.Transparent,
point.BackHatchStyle,
point.BackImage,
point.BackImageWrapMode,
point.BackImageTransparentColor,
point.BackImageAlignment,
point.BackGradientStyle,
(drawSegment) ? point.BackSecondaryColor : Color.Transparent,
(drawSegment) ? point.BorderColor : Color.Transparent,
point.BorderWidth,
point.BorderDashStyle,
PenAlignment.Center,
(drawSegmentShadow) ? point.series.ShadowOffset : 0,
point.series.ShadowColor);
}
if( this.Common.ProcessModeRegions )
{
// Add hot region
this.Common.HotRegionsList.AddHotRegion(
segmentPath,
false,
this.Graph,
point,
point.series.Name,
pointIndex);
}
segmentPath.Dispose();
// Add top 3D surface
if(this._rotation3D > 0f && startWidth > 0f && nothingOnTop)
{
if(this.Area.Area3DStyle.Enable3D)
{
PointF[] sidePoints = new PointF[4];
sidePoints[0] = new PointF(xCenterPointAbs + startWidth / 2f, location);
sidePoints[1] = new PointF(xCenterPointAbs, location + topRotationHeight);
sidePoints[2] = new PointF(xCenterPointAbs - startWidth / 2f, location);
sidePoints[3] = new PointF(xCenterPointAbs, location - topRotationHeight);
GraphicsPath topCurve = new GraphicsPath();
topCurve.AddLines(sidePoints);
topCurve.CloseAllFigures();
if( this.Common.ProcessModePaint )
{
// Fill graphics path
this.Graph.DrawPathAbs(
topCurve,
(drawSegment) ? ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.4 ) : Color.Transparent,
point.BackHatchStyle,
point.BackImage,
point.BackImageWrapMode,
point.BackImageTransparentColor,
point.BackImageAlignment,
point.BackGradientStyle,
(drawSegment) ? point.BackSecondaryColor : Color.Transparent,
(drawSegment) ? point.BorderColor : Color.Transparent,
point.BorderWidth,
point.BorderDashStyle,
PenAlignment.Center,
(drawSegmentShadow) ? point.series.ShadowOffset : 0,
point.series.ShadowColor);
}
if( this.Common.ProcessModeRegions )
{
// Add hot region
this.Common.HotRegionsList.AddHotRegion(
topCurve,
false,
this.Graph,
point,
point.series.Name,
pointIndex);
}
topCurve.Dispose();
}
}
// Add bottom 3D surface
if(this._rotation3D < 0f && startWidth > 0f && nothingOnBottom)
{
if(this.Area.Area3DStyle.Enable3D)
{
PointF[] sidePoints = new PointF[4];
sidePoints[0] = new PointF(xCenterPointAbs + endWidth / 2f, location + height);
sidePoints[1] = new PointF(xCenterPointAbs, location + height + bottomRotationHeight);
sidePoints[2] = new PointF(xCenterPointAbs - endWidth / 2f, location + height);
sidePoints[3] = new PointF(xCenterPointAbs, location + height - bottomRotationHeight);
GraphicsPath topCurve = new GraphicsPath();
topCurve.AddLines(sidePoints);
topCurve.CloseAllFigures();
if( this.Common.ProcessModePaint )
{
// Fill graphics path
this.Graph.DrawPathAbs(
topCurve,
(drawSegment) ? ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.4 ) : Color.Transparent,
point.BackHatchStyle,
point.BackImage,
point.BackImageWrapMode,
point.BackImageTransparentColor,
point.BackImageAlignment,
point.BackGradientStyle,
(drawSegment) ? point.BackSecondaryColor : Color.Transparent,
(drawSegment) ? point.BorderColor : Color.Transparent,
point.BorderWidth,
point.BorderDashStyle,
PenAlignment.Center,
(drawSegmentShadow) ? point.series.ShadowOffset : 0,
point.series.ShadowColor);
}
if( this.Common.ProcessModeRegions )
{
// Add hot region
this.Common.HotRegionsList.AddHotRegion(
topCurve,
false,
this.Graph,
point,
point.series.Name,
pointIndex);
}
topCurve.Dispose();
}
}
// End Svg Selection mode
this.Graph.EndHotRegion( );
}
/// <summary>
/// Draws funnel segment.
/// </summary>
/// <param name="point">Data point</param>
/// <param name="pointIndex">Data point index.</param>
/// <param name="startWidth">Segment top width.</param>
/// <param name="endWidth">Segment bottom width.</param>
/// <param name="location">Segment top location.</param>
/// <param name="height">Segment height.</param>
/// <param name="nothingOnTop">True if nothing is on the top of that segment.</param>
/// <param name="nothingOnBottom">True if nothing is on the bottom of that segment.</param>
/// <param name="drawSegment">True if segment shadow should be drawn.</param>
/// <param name="drawSegmentShadow">True if segment shadow should be drawn.</param>
private void DrawFunnelCircularSegment(
DataPoint point,
int pointIndex,
float startWidth,
float endWidth,
float location,
float height,
bool nothingOnTop,
bool nothingOnBottom,
bool drawSegment,
bool drawSegmentShadow)
{
PointF leftSideLinePoint = PointF.Empty;
PointF rightSideLinePoint = PointF.Empty;
// Check if square 3D segment should be drawn
if(this.Area.Area3DStyle.Enable3D && !round3DShape)
{
DrawFunnel3DSquareSegment(
point,
pointIndex,
startWidth,
endWidth,
location,
height,
nothingOnTop,
nothingOnBottom,
drawSegment,
drawSegmentShadow);
return;
}
// Increase the height of the segment to make sure there is no gaps between segments
if(!nothingOnBottom)
{
height += 0.3f;
}
// Segment width can't be smaller than funnel neck width
float originalStartWidth = startWidth;
float originalEndWidth = endWidth;
if( this._funnelStyle == FunnelStyle.YIsHeight && !this.isPyramid)
{
if(startWidth < this._funnelNeckSize.Width)
{
startWidth = this._funnelNeckSize.Width;
}
if(endWidth < this._funnelNeckSize.Width)
{
endWidth = this._funnelNeckSize.Width;
}
}
// Get 3D rotation angle
float tension = 0.8f;
float topRotationHeight = (float)( (startWidth / 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) );
float bottomRotationHeight = (float)( (endWidth / 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) );
// Get plotting area position in pixels
RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(this.PlotAreaPosition);
// Get the horizontal center point in pixels
float xCenterPointAbs = plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f;
// Start Svg Selection mode
this.Graph.StartHotRegion( point );
// Create segment path
GraphicsPath segmentPath = new GraphicsPath();
// Add top line
if(startWidth > 0f)
{
if(this.Area.Area3DStyle.Enable3D)
{
PointF[] sidePoints = new PointF[4];
sidePoints[0] = new PointF(xCenterPointAbs + startWidth / 2f, location);
sidePoints[1] = new PointF(xCenterPointAbs, location + topRotationHeight);
sidePoints[2] = new PointF(xCenterPointAbs - startWidth / 2f, location);
sidePoints[3] = new PointF(xCenterPointAbs, location - topRotationHeight);
GraphicsPath topCurve = new GraphicsPath();
topCurve.AddClosedCurve(sidePoints, tension);
topCurve.Flatten();
topCurve.Reverse();
Graph.AddEllipseSegment(
segmentPath,
topCurve,
null,
true,
0f,
out leftSideLinePoint,
out rightSideLinePoint);
}
else
{
segmentPath.AddLine(
xCenterPointAbs - startWidth / 2f, location,
xCenterPointAbs + startWidth / 2f, location);
}
}
// Add right line
if( this._funnelStyle == FunnelStyle.YIsHeight &&
!this.isPyramid &&
startWidth > this._funnelNeckSize.Width &&
endWidth <= this._funnelNeckSize.Width)
{
// Get intersection point of the vertical line at the neck border
// with the left pre-defined wall of the funnel.
PointF intersection = ChartGraphics.GetLinesIntersection(
xCenterPointAbs + this._funnelNeckSize.Width / 2f, plotAreaPositionAbs.Top,
xCenterPointAbs + this._funnelNeckSize.Width / 2f, plotAreaPositionAbs.Bottom,
xCenterPointAbs + originalStartWidth / 2f, location,
xCenterPointAbs + originalEndWidth / 2f, location + height);
// Adjust intersection point with top of the neck
intersection.Y = plotAreaPositionAbs.Bottom - this._funnelNeckSize.Height;
// Add two segment line
segmentPath.AddLine(
xCenterPointAbs + startWidth / 2f, location,
intersection.X, intersection.Y);
segmentPath.AddLine(
intersection.X, intersection.Y,
intersection.X, location + height);
}
else
{
// Add straight line
segmentPath.AddLine(
xCenterPointAbs + startWidth / 2f, location,
xCenterPointAbs + endWidth / 2f, location + height);
}
// Add bottom line
if(endWidth > 0f)
{
if(this.Area.Area3DStyle.Enable3D)
{
PointF[] sidePoints = new PointF[4];
sidePoints[0] = new PointF(xCenterPointAbs + endWidth / 2f, location + height);
sidePoints[1] = new PointF(xCenterPointAbs, location + height + bottomRotationHeight);
sidePoints[2] = new PointF(xCenterPointAbs - endWidth / 2f, location + height);
sidePoints[3] = new PointF(xCenterPointAbs, location + height - bottomRotationHeight);
GraphicsPath topCurve = new GraphicsPath();
topCurve.AddClosedCurve(sidePoints, tension);
topCurve.Flatten();
topCurve.Reverse();
using (GraphicsPath tmp = new GraphicsPath())
{
Graph.AddEllipseSegment(
tmp,
topCurve,
null,
true,
0f,
out leftSideLinePoint,
out rightSideLinePoint);
tmp.Reverse();
if (tmp.PointCount > 0)
{
segmentPath.AddPath(tmp, false);
}
}
}
else
{
segmentPath.AddLine(
xCenterPointAbs + endWidth / 2f, location + height,
xCenterPointAbs - endWidth / 2f, location + height);
}
}
// Add left line
if( this._funnelStyle == FunnelStyle.YIsHeight &&
!this.isPyramid &&
startWidth > this._funnelNeckSize.Width &&
endWidth <= this._funnelNeckSize.Width)
{
// Get intersection point of the horizontal line at the start of the segment
// with the left pre-defined wall of the funnel.
PointF intersection = ChartGraphics.GetLinesIntersection(
xCenterPointAbs - this._funnelNeckSize.Width / 2f, plotAreaPositionAbs.Top,
xCenterPointAbs - this._funnelNeckSize.Width / 2f, plotAreaPositionAbs.Bottom,
xCenterPointAbs - originalStartWidth / 2f, location,
xCenterPointAbs - originalEndWidth / 2f, location + height);
// Adjust intersection point with top of the neck
intersection.Y = plotAreaPositionAbs.Bottom - this._funnelNeckSize.Height;
// Add two segment line
segmentPath.AddLine(
intersection.X, location + height,
intersection.X, intersection.Y);
segmentPath.AddLine(
intersection.X, intersection.Y,
xCenterPointAbs - startWidth / 2f, location);
}
else
{
segmentPath.AddLine(
xCenterPointAbs - endWidth / 2f, location + height,
xCenterPointAbs - startWidth / 2f, location);
}
if( this.Common.ProcessModePaint )
{
// Draw lightStyle source blink effect in 3D
if(this.Area.Area3DStyle.Enable3D &&
Graph.ActiveRenderingType == RenderingType.Gdi )
{
// Get lighter and darker back colors
Color lightColor = ChartGraphics.GetGradientColor( point.Color, Color.White, 0.3 );
Color darkColor = ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.3 );
// Create linear gradient brush
RectangleF boundsRect = segmentPath.GetBounds();
if(boundsRect.Width == 0f)
{
boundsRect.Width = 1f;
}
if(boundsRect.Height == 0f)
{
boundsRect.Height = 1f;
}
using( LinearGradientBrush brush = new LinearGradientBrush(
boundsRect,
lightColor,
darkColor,
0f) )
{
// Set linear gradient brush interpolation colors
ColorBlend colorBlend = new ColorBlend(5);
colorBlend.Colors[0] = darkColor;
colorBlend.Colors[1] = darkColor;
colorBlend.Colors[2] = lightColor;
colorBlend.Colors[3] = darkColor;
colorBlend.Colors[4] = darkColor;
colorBlend.Positions[0] = 0.0f;
colorBlend.Positions[1] = 0.0f;
colorBlend.Positions[2] = 0.5f;
colorBlend.Positions[3] = 1.0f;
colorBlend.Positions[4] = 1.0f;
brush.InterpolationColors = colorBlend;
// Fill path
this.Graph.Graphics.FillPath(brush, segmentPath);
// Draw path border
Pen pen = new Pen(point.BorderColor, point.BorderWidth);
pen.DashStyle = this.Graph.GetPenStyle( point.BorderDashStyle );
if(point.BorderWidth == 0 ||
point.BorderDashStyle == ChartDashStyle.NotSet ||
point.BorderColor == Color.Empty)
{
// Draw line of the darker color inside the cylinder
pen = new Pen(ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.3 ), 1);
pen.Alignment = PenAlignment.Inset;
}
pen.StartCap = LineCap.Round;
pen.EndCap = LineCap.Round;
pen.LineJoin = LineJoin.Bevel;
this.Graph.DrawPath(pen, segmentPath );
pen.Dispose();
}
}
else
{
// Fill graphics path
this.Graph.DrawPathAbs(
segmentPath,
(drawSegment) ? point.Color : Color.Transparent,
point.BackHatchStyle,
point.BackImage,
point.BackImageWrapMode,
point.BackImageTransparentColor,
point.BackImageAlignment,
point.BackGradientStyle,
(drawSegment) ? point.BackSecondaryColor : Color.Transparent,
(drawSegment) ? point.BorderColor : Color.Transparent,
point.BorderWidth,
point.BorderDashStyle,
PenAlignment.Center,
(drawSegmentShadow) ? point.series.ShadowOffset : 0,
point.series.ShadowColor);
}
}
if( this.Common.ProcessModeRegions )
{
// Add hot region
this.Common.HotRegionsList.AddHotRegion(
segmentPath,
false,
this.Graph,
point,
point.series.Name,
pointIndex);
}
segmentPath.Dispose();
// Add top 3D surface
if(this._rotation3D > 0f && startWidth > 0f && nothingOnTop)
{
if(this.Area.Area3DStyle.Enable3D)
{
PointF[] sidePoints = new PointF[4];
sidePoints[0] = new PointF(xCenterPointAbs + startWidth / 2f, location);
sidePoints[1] = new PointF(xCenterPointAbs, location + topRotationHeight);
sidePoints[2] = new PointF(xCenterPointAbs - startWidth / 2f, location);
sidePoints[3] = new PointF(xCenterPointAbs, location - topRotationHeight);
GraphicsPath topCurve = new GraphicsPath();
topCurve.AddClosedCurve(sidePoints, tension);
if( this.Common.ProcessModePaint )
{
// Fill graphics path
this.Graph.DrawPathAbs(
topCurve,
(drawSegment) ? ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.4 ) : Color.Transparent,
point.BackHatchStyle,
point.BackImage,
point.BackImageWrapMode,
point.BackImageTransparentColor,
point.BackImageAlignment,
point.BackGradientStyle,
(drawSegment) ? point.BackSecondaryColor : Color.Transparent,
(drawSegment) ? point.BorderColor : Color.Transparent,
point.BorderWidth,
point.BorderDashStyle,
PenAlignment.Center,
(drawSegmentShadow) ? point.series.ShadowOffset : 0,
point.series.ShadowColor);
}
if( this.Common.ProcessModeRegions )
{
// Add hot region
this.Common.HotRegionsList.AddHotRegion(
topCurve,
false,
this.Graph,
point,
point.series.Name,
pointIndex);
}
topCurve.Dispose();
}
}
// Add bottom 3D surface
if(this._rotation3D < 0f && startWidth > 0f && nothingOnBottom)
{
if(this.Area.Area3DStyle.Enable3D)
{
PointF[] sidePoints = new PointF[4];
sidePoints[0] = new PointF(xCenterPointAbs + endWidth / 2f, location + height);
sidePoints[1] = new PointF(xCenterPointAbs, location + height + bottomRotationHeight);
sidePoints[2] = new PointF(xCenterPointAbs - endWidth / 2f, location + height);
sidePoints[3] = new PointF(xCenterPointAbs, location + height - bottomRotationHeight);
GraphicsPath topCurve = new GraphicsPath();
topCurve.AddClosedCurve(sidePoints, tension);
if( this.Common.ProcessModePaint )
{
// Fill graphics path
this.Graph.DrawPathAbs(
topCurve,
(drawSegment) ? ChartGraphics.GetGradientColor( point.Color, Color.Black, 0.4 ) : Color.Transparent,
point.BackHatchStyle,
point.BackImage,
point.BackImageWrapMode,
point.BackImageTransparentColor,
point.BackImageAlignment,
point.BackGradientStyle,
(drawSegment) ? point.BackSecondaryColor : Color.Transparent,
(drawSegment) ? point.BorderColor : Color.Transparent,
point.BorderWidth,
point.BorderDashStyle,
PenAlignment.Center,
(drawSegmentShadow) ? point.series.ShadowOffset : 0,
point.series.ShadowColor);
}
if( this.Common.ProcessModeRegions )
{
// Add hot region
this.Common.HotRegionsList.AddHotRegion(
topCurve,
false,
this.Graph,
point,
point.series.Name,
pointIndex);
}
topCurve.Dispose();
}
}
// End Svg Selection mode
this.Graph.EndHotRegion( );
}
/// <summary>
/// Fill list with information about every segment of the funnel.
/// </summary>
/// <returns>Funnel segment information list.</returns>
private ArrayList GetFunnelSegmentPositions()
{
// Create new list
ArrayList list = new ArrayList();
// Funnel chart process only first series in the chart area
// and cannot be combined with any other chart types.
Series series = GetDataSeries();
if( series != null )
{
// Get funnel drawing style
this._funnelStyle = GetFunnelStyle(series);
// Check if round or square base is used in 3D chart
this.round3DShape = (GetFunnel3DDrawingStyle(series) == Funnel3DDrawingStyle.CircularBase);
// Get funnel points gap
this.funnelSegmentGap = GetFunnelPointGap(series);
// Get funnel neck size
this._funnelNeckSize = GetFunnelNeckSize(series);
// Loop through all ponts in the data series
float currentLocation = this.Graph.GetAbsolutePoint(this.PlotAreaPosition.Location).Y;
if(this.isPyramid)
{
// Pyramid is drawn in reversed order.
currentLocation = this.Graph.GetAbsoluteRectangle(this.PlotAreaPosition).Bottom;
}
for( int pointIndex = 0; pointIndex >= 0 && pointIndex < series.Points.Count; pointIndex += 1 )
{
DataPoint point = series.Points[pointIndex];
// Check if first data point should be drawn
if( pointIndex > 0 || ShouldDrawFirstPoint() )
{
// Get height and width of each data point segment
float startWidth = 0f;
float endWidth = 0f;
float height = 0f;
GetPointWidthAndHeight(
series,
pointIndex,
currentLocation,
out height,
out startWidth,
out endWidth);
// Check visibility of previous and next points
bool nothingOnTop = false;
bool nothingOnBottom = false;
if(this.funnelSegmentGap > 0)
{
nothingOnTop = true;
nothingOnBottom = true;
}
else
{
if(ShouldDrawFirstPoint())
{
if(pointIndex == 0 ||
series.Points[pointIndex-1].Color.A != 255)
{
if(this.isPyramid)
{
nothingOnBottom = true;
}
else
{
nothingOnTop = true;
}
}
}
else
{
if(pointIndex == 1 ||
series.Points[pointIndex-1].Color.A != 255)
{
if(this.isPyramid)
{
nothingOnBottom = true;
}
else
{
nothingOnTop = true;
}
}
}
if( pointIndex == series.Points.Count - 1)
{
if(this.isPyramid)
{
nothingOnTop = true;
}
else
{
nothingOnBottom = true;
}
}
else if(series.Points[pointIndex+1].Color.A != 255)
{
if(this.isPyramid)
{
nothingOnTop = true;
}
else
{
nothingOnBottom = true;
}
}
}
// Add segment information
FunnelSegmentInfo info = new FunnelSegmentInfo();
info.Point = point;
info.PointIndex = pointIndex;
info.StartWidth = startWidth;
info.EndWidth = endWidth;
info.Location = (this.isPyramid) ? currentLocation - height : currentLocation;
info.Height = height;
info.NothingOnTop = nothingOnTop;
info.NothingOnBottom = nothingOnBottom;
list.Add(info);
// Increase current Y location
if(this.isPyramid)
{
currentLocation -= height + this.funnelSegmentGap;
}
else
{
currentLocation += height + this.funnelSegmentGap;
}
}
}
}
return list;
}
#endregion
#region Labels Methods
/// <summary>
/// Draws funnel data point labels.
/// </summary>
private void DrawLabels()
{
// Loop through all labels
foreach(FunnelPointLabelInfo labelInfo in this.labelInfoList)
{
if(!labelInfo.Position.IsEmpty &&
!float.IsNaN(labelInfo.Position.X) &&
!float.IsNaN(labelInfo.Position.Y) &&
!float.IsNaN(labelInfo.Position.Width) &&
!float.IsNaN(labelInfo.Position.Height) )
{
// Start Svg Selection mode
this.Graph.StartHotRegion( labelInfo.Point );
// Get size of a single character used for spacing
SizeF spacing = this.Graph.MeasureString(
"W",
labelInfo.Point.Font,
new SizeF(1000f, 1000F),
StringFormat.GenericTypographic );
// Draw a callout line
if( !labelInfo.CalloutPoint1.IsEmpty &&
!labelInfo.CalloutPoint2.IsEmpty &&
!float.IsNaN(labelInfo.CalloutPoint1.X) &&
!float.IsNaN(labelInfo.CalloutPoint1.Y) &&
!float.IsNaN(labelInfo.CalloutPoint2.X) &&
!float.IsNaN(labelInfo.CalloutPoint2.Y) )
{
// Add spacing between text and callout line
if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right)
{
labelInfo.CalloutPoint2.X -= spacing.Width / 2f;
// Add a small spacing between a callout line and a segment
labelInfo.CalloutPoint1.X += 2;
}
else
{
labelInfo.CalloutPoint2.X += spacing.Width / 2f;
// Add a small spacing between a callout line and a segment
labelInfo.CalloutPoint1.X += 2;
}
// Get callout line color
Color lineColor = GetCalloutLineColor(labelInfo.Point);
// Draw callout line
this.Graph.DrawLineAbs(
lineColor,
1,
ChartDashStyle.Solid,
labelInfo.CalloutPoint1,
labelInfo.CalloutPoint2 );
}
// Get label background position
RectangleF labelBackPosition = labelInfo.Position;
labelBackPosition.Inflate(spacing.Width / 2f, spacing.Height / 8f);
labelBackPosition = this.Graph.GetRelativeRectangle(labelBackPosition);
// Center label in the middle of the background rectangle
using (StringFormat format = new StringFormat())
{
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
// Draw label text
using (Brush brush = new SolidBrush(labelInfo.Point.LabelForeColor))
{
this.Graph.DrawPointLabelStringRel(
this.Common,
labelInfo.Text,
labelInfo.Point.Font,
brush,
labelBackPosition,
format,
labelInfo.Point.LabelAngle,
labelBackPosition,
labelInfo.Point.LabelBackColor,
labelInfo.Point.LabelBorderColor,
labelInfo.Point.LabelBorderWidth,
labelInfo.Point.LabelBorderDashStyle,
labelInfo.Point.series,
labelInfo.Point,
labelInfo.PointIndex);
}
// End Svg Selection mode
this.Graph.EndHotRegion();
}
}
}
}
/// <summary>
/// Creates a list of structures with the data point labels information.
/// </summary>
/// <returns>Array list of labels information.</returns>
private ArrayList CreateLabelsInfoList()
{
ArrayList list = new ArrayList();
// Get area position in pixels
RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle( this.Area.Position.ToRectangleF() );
// Get funnel chart type series
Series series = GetDataSeries();
if( series != null )
{
// Loop through all ponts in the data series
int pointIndex = 0;
foreach( DataPoint point in series.Points )
{
// Ignore empty points
if( !point.IsEmpty )
{
// Get some properties for performance
string pointLabel = point.Label;
bool pointShowLabelAsValue = point.IsValueShownAsLabel;
// Check if label text exists
if(pointShowLabelAsValue || pointLabel.Length > 0)
{
// Create new point label information class
FunnelPointLabelInfo labelInfo = new FunnelPointLabelInfo();
labelInfo.Point = point;
labelInfo.PointIndex = pointIndex;
// Get point label text
if( pointLabel.Length == 0 )
{
labelInfo.Text = ValueConverter.FormatValue(
point.series.Chart,
point,
point.Tag,
point.YValues[0],
point.LabelFormat,
point.series.YValueType,
ChartElementType.DataPoint);
}
else
{
labelInfo.Text = point.ReplaceKeywords(pointLabel);
}
// Get label style
labelInfo.Style = GetLabelStyle(point);
// Get inside label vertical alignment
if(labelInfo.Style == FunnelLabelStyle.Inside)
{
labelInfo.VerticalAlignment = GetInsideLabelAlignment(point);
}
// Get outside labels placement
if(labelInfo.Style != FunnelLabelStyle.Inside)
{
labelInfo.OutsidePlacement = GetOutsideLabelPlacement(point);
}
// Measure string size
labelInfo.Size = this.Graph.MeasureString(
labelInfo.Text,
point.Font,
plotAreaPositionAbs.Size,
StringFormat.GenericTypographic);
// Add label information into the list
if(labelInfo.Text.Length > 0 &&
labelInfo.Style != FunnelLabelStyle.Disabled)
{
list.Add(labelInfo);
}
}
}
++pointIndex;
}
}
return list;
}
/// <summary>
/// Changes required plotting area spacing, so that all labels fit.
/// </summary>
/// <returns>Return True if no resizing required.</returns>
private bool FitPointLabels()
{
// Convert plotting area position to pixels.
// Make rectangle 4 pixels smaller on each side.
RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(PlotAreaPosition);
plotAreaPositionAbs.Inflate(-4f, -4f);
// Get position of each label
GetLabelsPosition();
// Get spacing required to draw labels
RectangleF requiredSpacing = this.Graph.GetAbsoluteRectangle( new RectangleF(1f, 1f, 1f, 1f) );
foreach(FunnelPointLabelInfo labelInfo in this.labelInfoList)
{
// Add additional horizontal spacing for outside labels
RectangleF position = labelInfo.Position;
if(labelInfo.Style == FunnelLabelStyle.Outside ||
labelInfo.Style == FunnelLabelStyle.OutsideInColumn)
{
float spacing = 10f;
if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right)
{
position.Width += spacing;
}
else if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Left)
{
position.X -= spacing;
position.Width += spacing;
}
}
// Horizontal coordinates are ignored for Inside label style
if(labelInfo.Style != FunnelLabelStyle.Inside)
{
if( (plotAreaPositionAbs.X - position.X) > requiredSpacing.X )
{
requiredSpacing.X = plotAreaPositionAbs.X - position.X;
}
if( (position.Right - plotAreaPositionAbs.Right) > requiredSpacing.Width )
{
requiredSpacing.Width = position.Right - plotAreaPositionAbs.Right;
}
}
// Vertical spacing
if( (plotAreaPositionAbs.Y - position.Y) > requiredSpacing.Y )
{
requiredSpacing.Y = plotAreaPositionAbs.Y - position.Y;
}
if( (position.Bottom - plotAreaPositionAbs.Bottom) > requiredSpacing.Height )
{
requiredSpacing.Height = position.Bottom - plotAreaPositionAbs.Bottom;
}
}
// Convert spacing rectangle to relative coordinates
requiredSpacing = this.Graph.GetRelativeRectangle(requiredSpacing);
// Check if non-default spacing was used
if(requiredSpacing.X > 1f ||
requiredSpacing.Y > 1f ||
requiredSpacing.Width > 1f ||
requiredSpacing.Height > 1f )
{
this.plotAreaSpacing = requiredSpacing;
// Get NEW plotting area position
this.PlotAreaPosition = GetPlotAreaPosition();
// Get NEW list of segments
this.segmentList = GetFunnelSegmentPositions();
// Get NEW position of each label
GetLabelsPosition();
return false;
}
return true;
}
/// <summary>
/// Loops through the point labels list and calculates labels position
/// based on their size, position and funnel chart shape.
/// </summary>
private void GetLabelsPosition()
{
// Convert plotting area position to pixels
RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(PlotAreaPosition);
float plotAreaCenterXAbs = plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f;
// Define label spacing
SizeF labelSpacing = new SizeF(3f, 3f);
//Loop through all labels
foreach(FunnelPointLabelInfo labelInfo in this.labelInfoList)
{
// Get ----osiated funnel segment information
bool lastLabel = false;
int pointIndex = labelInfo.PointIndex + ((ShouldDrawFirstPoint()) ? 0 : 1);
if(pointIndex > this.segmentList.Count && !ShouldDrawFirstPoint() )
{
// Use last point index if first point is not drawn
pointIndex = this.segmentList.Count;
lastLabel = true;
}
FunnelSegmentInfo segmentInfo = null;
foreach(FunnelSegmentInfo info in this.segmentList)
{
if(info.PointIndex == pointIndex)
{
segmentInfo = info;
break;
}
}
// Check if segment was found
if(segmentInfo != null)
{
// Set label width and height
labelInfo.Position.Width = labelInfo.Size.Width;
labelInfo.Position.Height = labelInfo.Size.Height;
//******************************************************
//** Labels are placed OUTSIDE of the funnel
//******************************************************
if(labelInfo.Style == FunnelLabelStyle.Outside ||
labelInfo.Style == FunnelLabelStyle.OutsideInColumn)
{
// Define position
if( this._funnelStyle == FunnelStyle.YIsHeight )
{
// Get segment top and bottom diameter
float topDiameter = segmentInfo.StartWidth;
float bottomDiameter = segmentInfo.EndWidth;
if(!this.isPyramid)
{
if(topDiameter < this._funnelNeckSize.Width)
{
topDiameter = this._funnelNeckSize.Width;
}
if(bottomDiameter < this._funnelNeckSize.Width)
{
bottomDiameter = this._funnelNeckSize.Width;
}
// Adjust label position because segment is bent to make a neck
if(segmentInfo.StartWidth >= this._funnelNeckSize.Width &&
segmentInfo.EndWidth < this._funnelNeckSize.Width)
{
bottomDiameter = segmentInfo.EndWidth;
}
}
// Get Y position
labelInfo.Position.Y = (segmentInfo.Location + segmentInfo.Height / 2f) -
labelInfo.Size.Height / 2f;
// Get X position
if(labelInfo.Style == FunnelLabelStyle.OutsideInColumn)
{
if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right)
{
labelInfo.Position.X = plotAreaPositionAbs.Right +
4f * labelSpacing.Width;
// Set callout line coordinates
if(!this.isPyramid)
{
labelInfo.CalloutPoint1.X = plotAreaCenterXAbs +
Math.Max(this._funnelNeckSize.Width/2f, (topDiameter + bottomDiameter) / 4f);
}
else
{
labelInfo.CalloutPoint1.X = plotAreaCenterXAbs +
(topDiameter + bottomDiameter) / 4f;
}
labelInfo.CalloutPoint2.X = labelInfo.Position.X;
}
else
{
labelInfo.Position.X = plotAreaPositionAbs.X -
labelInfo.Size.Width -
4f * labelSpacing.Width;
// Set callout line coordinates
if(!this.isPyramid)
{
labelInfo.CalloutPoint1.X = plotAreaCenterXAbs -
Math.Max(this._funnelNeckSize.Width/2f, (topDiameter + bottomDiameter) / 4f);
}
else
{
labelInfo.CalloutPoint1.X = plotAreaCenterXAbs -
(topDiameter + bottomDiameter) / 4f;
}
labelInfo.CalloutPoint2.X = labelInfo.Position.Right;
}
// Fill rest of coordinates required for the callout line
labelInfo.CalloutPoint1.Y = segmentInfo.Location + segmentInfo.Height / 2f;
labelInfo.CalloutPoint2.Y = labelInfo.CalloutPoint1.Y;
}
else
{
if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right)
{
labelInfo.Position.X = plotAreaCenterXAbs +
(topDiameter + bottomDiameter) / 4f +
4f * labelSpacing.Width;
}
else
{
labelInfo.Position.X = plotAreaCenterXAbs -
labelInfo.Size.Width -
(topDiameter + bottomDiameter) / 4f -
4f * labelSpacing.Width;
}
}
}
else
{
// Use bottom part of the segment for the last point
if(lastLabel)
{
if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right)
{
labelInfo.Position.X = plotAreaCenterXAbs +
segmentInfo.EndWidth / 2f +
4f * labelSpacing.Width;
}
else
{
labelInfo.Position.X = plotAreaCenterXAbs -
labelInfo.Size.Width -
segmentInfo.EndWidth / 2f -
4f * labelSpacing.Width;
}
labelInfo.Position.Y = segmentInfo.Location +
segmentInfo.Height -
labelInfo.Size.Height / 2f;
}
else
{
if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right)
{
labelInfo.Position.X = plotAreaCenterXAbs +
segmentInfo.StartWidth / 2f +
4f * labelSpacing.Width;
}
else
{
labelInfo.Position.X = plotAreaCenterXAbs -
labelInfo.Size.Width -
segmentInfo.StartWidth / 2f -
4f * labelSpacing.Width;
}
labelInfo.Position.Y = segmentInfo.Location -
labelInfo.Size.Height / 2f;
}
if(labelInfo.Style == FunnelLabelStyle.OutsideInColumn)
{
if(labelInfo.OutsidePlacement == FunnelLabelPlacement.Right)
{
labelInfo.Position.X = plotAreaPositionAbs.Right +
4f * labelSpacing.Width;
// Set callout line coordinates
labelInfo.CalloutPoint1.X = plotAreaCenterXAbs +
( (lastLabel) ? segmentInfo.EndWidth : segmentInfo.StartWidth) / 2f;
labelInfo.CalloutPoint2.X = labelInfo.Position.X;
}
else
{
labelInfo.Position.X = plotAreaPositionAbs.X -
labelInfo.Size.Width -
4f * labelSpacing.Width;
// Set callout line coordinates
labelInfo.CalloutPoint1.X = plotAreaCenterXAbs -
( (lastLabel) ? segmentInfo.EndWidth : segmentInfo.StartWidth) / 2f;
labelInfo.CalloutPoint2.X = labelInfo.Position.Right;
}
// Fill rest of coordinates required for the callout line
labelInfo.CalloutPoint1.Y = segmentInfo.Location;
if(lastLabel)
{
labelInfo.CalloutPoint1.Y += segmentInfo.Height;
}
labelInfo.CalloutPoint2.Y = labelInfo.CalloutPoint1.Y;
}
}
}
//******************************************************
//** Labels are placed INSIDE of the funnel
//******************************************************
else if(labelInfo.Style == FunnelLabelStyle.Inside)
{
// Define position
labelInfo.Position.X = plotAreaCenterXAbs - labelInfo.Size.Width / 2f;
if( this._funnelStyle == FunnelStyle.YIsHeight )
{
labelInfo.Position.Y = (segmentInfo.Location + segmentInfo.Height / 2f) -
labelInfo.Size.Height / 2f;
if(labelInfo.VerticalAlignment == FunnelLabelVerticalAlignment.Top)
{
labelInfo.Position.Y -= segmentInfo.Height / 2f - labelInfo.Size.Height / 2f - labelSpacing.Height;
}
else if(labelInfo.VerticalAlignment == FunnelLabelVerticalAlignment.Bottom)
{
labelInfo.Position.Y += segmentInfo.Height / 2f - labelInfo.Size.Height / 2f - labelSpacing.Height;
}
}
else
{
labelInfo.Position.Y = segmentInfo.Location - labelInfo.Size.Height / 2f;
if(labelInfo.VerticalAlignment == FunnelLabelVerticalAlignment.Top)
{
labelInfo.Position.Y -= labelInfo.Size.Height / 2f + labelSpacing.Height;
}
else if(labelInfo.VerticalAlignment == FunnelLabelVerticalAlignment.Bottom)
{
labelInfo.Position.Y += labelInfo.Size.Height / 2f + labelSpacing.Height;
}
// Use bottom part of the segment for the last point
if(lastLabel)
{
labelInfo.Position.Y += segmentInfo.Height;
}
}
// Adjust label Y position in 3D
if(this.Area.Area3DStyle.Enable3D)
{
labelInfo.Position.Y += (float)( ( (segmentInfo.EndWidth + segmentInfo.StartWidth) / 4f) * Math.Sin(this._rotation3D / 180F * Math.PI) );
}
}
//******************************************************
//** Check if label overlaps any previous label
//******************************************************
int interation = 0;
while( IsLabelsOverlap(labelInfo) && interation < 1000)
{
float shiftSize = (this.isPyramid) ? -3f : 3f;
// Move label down
labelInfo.Position.Y += shiftSize;
// Move callout second point down
if(!labelInfo.CalloutPoint2.IsEmpty)
{
labelInfo.CalloutPoint2.Y += shiftSize;
}
++interation;
}
}
}
}
/// <summary>
/// Checks if specified label overlaps any previous labels.
/// </summary>
/// <param name="testLabelInfo">Label to test.</param>
/// <returns>True if labels overlapp.</returns>
private bool IsLabelsOverlap(FunnelPointLabelInfo testLabelInfo)
{
// Increase rectangle size by 1 pixel
RectangleF rect = testLabelInfo.Position;
rect.Inflate(1f, 1f);
// Increase label rectangle if border is drawn around the label
if(!testLabelInfo.Point.LabelBackColor.IsEmpty ||
(testLabelInfo.Point.LabelBorderWidth > 0 &&
!testLabelInfo.Point.LabelBorderColor.IsEmpty &&
testLabelInfo.Point.LabelBorderDashStyle != ChartDashStyle.NotSet) )
{
rect.Inflate(4f, 4f);
}
//Loop through all labels
foreach(FunnelPointLabelInfo labelInfo in this.labelInfoList)
{
// Stop searching
if(labelInfo.PointIndex == testLabelInfo.PointIndex)
{
break;
}
// Check if label position overlaps
if(!labelInfo.Position.IsEmpty &&
labelInfo.Position.IntersectsWith(rect) )
{
return true;
}
}
return false;
}
/// <summary>
/// Gets label style of the data point.
/// </summary>
/// <returns>Label style of the data point.</returns>
private FunnelLabelStyle GetLabelStyle(DataPointCustomProperties properties)
{
// Set default label style
FunnelLabelStyle labelStyle = FunnelLabelStyle.OutsideInColumn;
// Get string value of the custom attribute
string attrValue = properties[this.funnelLabelStyleAttributeName];
if(attrValue != null && attrValue.Length > 0)
{
// Convert string to the labels style
try
{
labelStyle = (FunnelLabelStyle)Enum.Parse(typeof(FunnelLabelStyle), attrValue, true);
}
catch
{
throw(new InvalidOperationException( SR.ExceptionCustomAttributeValueInvalid(labelStyle.ToString(), this.funnelLabelStyleAttributeName) ) );
}
}
return labelStyle;
}
#endregion // Labels Methods
#region Position Methods
/// <summary>
/// Calculate the spacing required for the labels.
/// </summary>
private void GetPlotAreaSpacing()
{
// Provide small spacing on the sides of chart area
this.plotAreaSpacing = new RectangleF(1f, 1f, 1f, 1f);
// Get plotting area position
this.PlotAreaPosition = GetPlotAreaPosition();
// Get list of segments
this.segmentList = GetFunnelSegmentPositions();
// If plotting area position is automatic
if( Area.InnerPlotPosition.Auto )
{
// Set a position so that data labels fit
// This method is called several time to adjust label position while
// funnel side angle is changed
int iteration = 0;
while(!FitPointLabels() && iteration < 5)
{
iteration++;
}
}
else
{
// Just get labels position
GetLabelsPosition();
}
}
/// <summary>
/// Gets a rectangle in relative coordinates where the funnel will chart
/// will be drawn.
/// </summary>
/// <returns>Plotting are of the chart in relative coordinates.</returns>
private RectangleF GetPlotAreaPosition()
{
// Get plotting area rectangle position
RectangleF plotAreaPosition = ( Area.InnerPlotPosition.Auto ) ?
Area.Position.ToRectangleF() : Area.PlotAreaPosition.ToRectangleF();
// NOTE: Fixes issue #4085
// Do not allow decreasing of the plot area height more than 50%
if(plotAreaSpacing.Y > plotAreaPosition.Height / 2f)
{
plotAreaSpacing.Y = plotAreaPosition.Height / 2f;
}
if(plotAreaSpacing.Height > plotAreaPosition.Height / 2f)
{
plotAreaSpacing.Height = plotAreaPosition.Height / 2f;
}
// Decrease plotting are position using pre-calculated ratio
plotAreaPosition.X += plotAreaSpacing.X;
plotAreaPosition.Y += plotAreaSpacing.Y;
plotAreaPosition.Width -= plotAreaSpacing.X + plotAreaSpacing.Width;
plotAreaPosition.Height -= plotAreaSpacing.Y + plotAreaSpacing.Height;
// Apply vertical spacing on top and bottom to fit the 3D surfaces
if(this.Area.Area3DStyle.Enable3D)
{
// Convert position to pixels
RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(plotAreaPosition);
// Funnel chart process only first series in the chart area
// and cannot be combined with any other chart types.
Series series = GetDataSeries();
if( series != null )
{
// Get 3D funnel rotation angle (from 10 to -10)
this._rotation3D = GetFunnelRotation(series);
}
// Get top and bottom spacing
float topSpacing = (float)Math.Abs( (plotAreaPositionAbs.Width/ 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) );
float bottomSpacing = (float)Math.Abs( (plotAreaPositionAbs.Width/ 2f) * Math.Sin(this._rotation3D / 180F * Math.PI) );
// Adjust position
if(this.isPyramid)
{
// Only bottom spacing for the pyramid
plotAreaPositionAbs.Height -= bottomSpacing;
}
else
{
// Add top/bottom spacing
plotAreaPositionAbs.Y += topSpacing;
plotAreaPositionAbs.Height -= topSpacing + bottomSpacing;
}
// Convert position back to relative coordinates
plotAreaPosition = this.Graph.GetRelativeRectangle(plotAreaPositionAbs);
}
return plotAreaPosition;
}
#endregion // Position Methods
#region Helper Methods
/// <summary>
/// Checks for minimum segment height.
/// </summary>
/// <param name="height">Current segment height.</param>
/// <returns>Adjusted segment height.</returns>
protected float CheckMinHeight(float height)
{
// When point gap is used do not allow to have the segment heigth to be zero.
float minSize = Math.Min(2f, this.funnelSegmentGap / 2f);
if(this.funnelSegmentGap > 0 &&
height < minSize)
{
return minSize;
}
return height;
}
/// <summary>
/// Gets minimum point height in pixels.
/// </summary>
/// <returns>Minimum point height in pixels.</returns>
private void GetFunnelMinPointHeight(DataPointCustomProperties properties)
{
// Set default minimum point size
this._funnelMinPointHeight = 0f;
// Get string value of the custom attribute
string attrValue = properties[this.funnelPointMinHeight];
if (attrValue != null && attrValue.Length > 0)
{
// Convert string to the point gap size
float pointHeight;
bool parseSucceed = float.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out pointHeight);
if (parseSucceed)
{
this._funnelMinPointHeight = pointHeight;
}
if (!parseSucceed || this._funnelMinPointHeight < 0f || this._funnelMinPointHeight > 100f)
{
throw (new InvalidOperationException(SR.ExceptionFunnelMinimumPointHeightAttributeInvalid));
}
// Check if specified value is too big
this._funnelMinPointHeight = (float)(this.yValueTotal * this._funnelMinPointHeight / 100f);
// Get data statistic again using Min value
GetDataPointValuesStatistic();
}
return;
}
/// <summary>
/// Gets 3D funnel rotation angle.
/// </summary>
/// <returns>Rotation angle.</returns>
private int GetFunnelRotation(DataPointCustomProperties properties)
{
// Set default gap size
int angle = 5;
// Get string value of the custom attribute
string attrValue = properties[this.funnelRotationAngleAttributeName];
if (attrValue != null && attrValue.Length > 0)
{
// Convert string to the point gap size
int a;
bool parseSucceed = int.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out a);
if (parseSucceed)
{
angle = a;
}
// Validate attribute value
if (!parseSucceed || angle < -10 || angle > 10)
{
throw (new InvalidOperationException(SR.ExceptionFunnelAngleRangeInvalid));
}
}
return angle;
}
/// <summary>
/// Gets callout line color.
/// </summary>
/// <returns>Callout line color.</returns>
private Color GetCalloutLineColor(DataPointCustomProperties properties)
{
// Set default gap size
Color color = Color.Black;
// Get string value of the custom attribute
string attrValue = properties[CustomPropertyName.CalloutLineColor];
if(attrValue != null && attrValue.Length > 0)
{
// Convert string to Color
bool failed = false;
ColorConverter colorConverter = new ColorConverter();
try
{
color = (Color)colorConverter.ConvertFromInvariantString(attrValue);
}
catch (ArgumentException)
{
failed = true;
}
catch (NotSupportedException)
{
failed = true;
}
// In case of an error try to convert using local settings
if(failed)
{
try
{
color = (Color)colorConverter.ConvertFromString(attrValue);
}
catch(ArgumentException)
{
throw(new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid( attrValue, "CalloutLineColor") ) );
}
}
}
return color;
}
/// <summary>
/// Gets funnel neck size when shape of the funnel do not change.
/// </summary>
/// <returns>Funnel neck width and height.</returns>
private SizeF GetFunnelNeckSize(DataPointCustomProperties properties)
{
// Set default gap size
SizeF neckSize = new SizeF(5f, 5f);
// Get string value of the custom attribute
string attrValue = properties[CustomPropertyName.FunnelNeckWidth];
if (attrValue != null && attrValue.Length > 0)
{
// Convert string to the point gap size
float w;
bool parseSucceed = float.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out w);
if (parseSucceed)
{
neckSize.Width = w;
}
// Validate attribute value
if (!parseSucceed || neckSize.Width < 0 || neckSize.Width > 100)
{
throw (new InvalidOperationException(SR.ExceptionFunnelNeckWidthInvalid));
}
}
// Get string value of the custom attribute
attrValue = properties[CustomPropertyName.FunnelNeckHeight];
if (attrValue != null && attrValue.Length > 0)
{
// Convert string to the point gap size
float h;
bool parseSucceed = float.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out h);
if (parseSucceed)
{
neckSize.Height = h;
}
if (!parseSucceed || neckSize.Height < 0 || neckSize.Height > 100)
{
throw (new InvalidOperationException(SR.ExceptionFunnelNeckHeightInvalid));
}
}
// Make sure the neck size do not exceed the plotting area size
if(neckSize.Height > this.PlotAreaPosition.Height/2f)
{
neckSize.Height = this.PlotAreaPosition.Height/2f;
}
if(neckSize.Width > this.PlotAreaPosition.Width/2f)
{
neckSize.Width = this.PlotAreaPosition.Width/2f;
}
// Convert from relative coordinates to pixels
return this.Graph.GetAbsoluteSize(neckSize);
}
/// <summary>
/// Gets gap between points in pixels.
/// </summary>
/// <returns>Gap between funnel points.</returns>
private float GetFunnelPointGap(DataPointCustomProperties properties)
{
// Set default gap size
float gapSize = 0f;
// Get string value of the custom attribute
string attrValue = properties[this.funnelPointGapAttributeName];
if (attrValue != null && attrValue.Length > 0)
{
// Convert string to the point gap size
float gs;
bool parseSucceed = float.TryParse(attrValue, NumberStyles.Any, CultureInfo.InvariantCulture, out gs);
if (parseSucceed)
{
gapSize = gs;
}
else
{
throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid(attrValue, this.funnelPointGapAttributeName)));
}
// Make sure the total gap size for all points do not exceed the total height of the plotting area
float maxGapSize = this.PlotAreaPosition.Height / (this.pointNumber - ((ShouldDrawFirstPoint()) ? 1 : 2));
if (gapSize > maxGapSize)
{
gapSize = maxGapSize;
}
if (gapSize < 0)
{
gapSize = 0;
}
// Convert from relative coordinates to pixels
gapSize = this.Graph.GetAbsoluteSize(new SizeF(gapSize, gapSize)).Height;
}
return gapSize;
}
/// <summary>
/// Gets funnel drawing style.
/// </summary>
/// <returns>funnel drawing style.</returns>
private FunnelStyle GetFunnelStyle(DataPointCustomProperties properties)
{
// Set default funnel drawing style
FunnelStyle drawingStyle = FunnelStyle.YIsHeight;
// Get string value of the custom attribute
if(!this.isPyramid)
{
string attrValue = properties[CustomPropertyName.FunnelStyle];
if(attrValue != null && attrValue.Length > 0)
{
// Convert string to the labels style
try
{
drawingStyle = (FunnelStyle)Enum.Parse(typeof(FunnelStyle), attrValue, true);
}
catch
{
throw(new InvalidOperationException( SR.ExceptionCustomAttributeValueInvalid( attrValue, "FunnelStyle") ) );
}
}
}
return drawingStyle;
}
/// <summary>
/// Gets outside labels placement.
/// </summary>
/// <returns>Outside labels placement.</returns>
private FunnelLabelPlacement GetOutsideLabelPlacement(DataPointCustomProperties properties)
{
// Set default vertical alignment for the inside labels
FunnelLabelPlacement placement = FunnelLabelPlacement.Right;
// Get string value of the custom attribute
string attrValue = properties[this.funnelOutsideLabelPlacementAttributeName];
if(attrValue != null && attrValue.Length > 0)
{
// Convert string to the labels placement
try
{
placement = (FunnelLabelPlacement)Enum.Parse(typeof(FunnelLabelPlacement), attrValue, true);
}
catch
{
throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid(attrValue, this.funnelOutsideLabelPlacementAttributeName )));
}
}
return placement;
}
/// <summary>
/// Gets inside labels vertical alignment.
/// </summary>
/// <returns>Inside labels vertical alignment.</returns>
private FunnelLabelVerticalAlignment GetInsideLabelAlignment(DataPointCustomProperties properties)
{
// Set default vertical alignment for the inside labels
FunnelLabelVerticalAlignment alignment = FunnelLabelVerticalAlignment.Center;
// Get string value of the custom attribute
string attrValue = properties[this.funnelInsideLabelAlignmentAttributeName];
if(attrValue != null && attrValue.Length > 0)
{
// Convert string to the labels style
try
{
alignment = (FunnelLabelVerticalAlignment)Enum.Parse(typeof(FunnelLabelVerticalAlignment), attrValue, true);
}
catch
{
throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid(attrValue, this.funnelInsideLabelAlignmentAttributeName)));
}
}
return alignment;
}
/// <summary>
/// Gets funnel 3D drawing style.
/// </summary>
/// <returns>funnel drawing style.</returns>
private Funnel3DDrawingStyle GetFunnel3DDrawingStyle(DataPointCustomProperties properties)
{
// Set default funnel drawing style
Funnel3DDrawingStyle drawingStyle = (this.isPyramid) ?
Funnel3DDrawingStyle.SquareBase : Funnel3DDrawingStyle.CircularBase;
// Get string value of the custom attribute
string attrValue = properties[funnel3DDrawingStyleAttributeName];
if(attrValue != null && attrValue.Length > 0)
{
// Convert string to the labels style
try
{
drawingStyle = (Funnel3DDrawingStyle)Enum.Parse(typeof(Funnel3DDrawingStyle), attrValue, true);
}
catch
{
throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid(attrValue, funnel3DDrawingStyleAttributeName) ) );
}
}
return drawingStyle;
}
/// <summary>
/// Get data point Y and X values statistics:
/// - Total of all Y values
/// - Total of all X values
/// - Maximum Y value
/// Negative values are treated as positive.
/// </summary>
private void GetDataPointValuesStatistic()
{
// Get funnel chart type series
Series series = GetDataSeries();
if( series != null )
{
// Reset values
this.yValueTotal = 0.0;
this._xValueTotal = 0.0;
this._yValueMax = 0.0;
this.pointNumber = 0;
// Get value type
this._valuePercentages = null;
PyramidValueType valueType = this.GetPyramidValueType( series );
if(valueType == PyramidValueType.Surface)
{
// Calculate the total surface area
double triangleArea = 0.0;
int pointIndex = 0;
foreach( DataPoint point in series.Points )
{
// Ignore empty points
if( !point.IsEmpty )
{
// Get Y value
triangleArea += GetYValue(point, pointIndex);
}
++pointIndex;
}
// Calculate the base
double triangleHeight = 100.0;
double triangleBase = (2* triangleArea) / triangleHeight;
// Calculate the base to height ratio
double baseRatio = triangleBase / triangleHeight;
// Calcuate the height percentage for each value
double[] percentages = new double[series.Points.Count];
double sumArea = 0.0;
for(int loop = 0; loop < percentages.Length; loop++)
{
double yValue = GetYValue(series.Points[loop], loop);
sumArea += yValue;
percentages[loop] = Math.Sqrt((2 * sumArea) / baseRatio);
}
this._valuePercentages = percentages;
}
// Loop through all ponts in the data series
foreach( DataPoint point in series.Points )
{
// Ignore empty points
if( !point.IsEmpty )
{
// Get Y value
double yValue = GetYValue(point, this.pointNumber);
// Get data point Y and X values statistics
this.yValueTotal += yValue;
this._yValueMax = Math.Max(this._yValueMax, yValue);
this._xValueTotal += GetXValue(point);
}
++this.pointNumber;
}
}
}
/// <summary>
/// Gets funnel chart series that belongs to the current chart area.
/// Method also checks that only one visible Funnel series exists in the chart area.
/// </summary>
/// <returns>Funnel chart type series.</returns>
private Series GetDataSeries()
{
// Check if funnel series was already found
if(this._chartTypeSeries == null)
{
// Loop through all series
Series funnelSeries = null;
foreach( Series series in Common.DataManager.Series )
{
// Check if series is visible and belong to the current chart area
if( series.IsVisible() &&
series.ChartArea == this.Area.Name )
{
// Check series chart type is Funnel
if( String.Compare( series.ChartTypeName, this.Name, true, System.Globalization.CultureInfo.CurrentCulture ) == 0 )
{
if(funnelSeries == null)
{
funnelSeries = series;
}
}
else if(!this.Common.ChartPicture.SuppressExceptions)
{
// Funnel chart can not be combined with other chart type
throw (new InvalidOperationException(SR.ExceptionFunnelCanNotCombine));
}
}
}
// Remember the chart type series
this._chartTypeSeries = funnelSeries;
}
return this._chartTypeSeries;
}
/// <summary>
/// Gets pyramid value type. Each point value may represent a "Linear" height of
/// the segment or "Surface" of the segment.
/// </summary>
/// <returns>Pyramid value type.</returns>
private PyramidValueType GetPyramidValueType(DataPointCustomProperties properties)
{
// Set default funnel drawing style
PyramidValueType valueType = PyramidValueType.Linear;
// Get string value of the custom attribute
if(this.isPyramid)
{
string attrValue = properties[CustomPropertyName.PyramidValueType];
if(attrValue != null && attrValue.Length > 0)
{
// Convert string to the labels style
try
{
valueType = (PyramidValueType)Enum.Parse(typeof(PyramidValueType), attrValue, true);
}
catch
{
throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid(attrValue,"PyramidValueType") ) );
}
}
}
return valueType;
}
#endregion // Helper Methods
#region Y & X values related methods
/// <summary>
/// Helper function, which returns the Y value of the point.
/// </summary>
/// <param name="point">Point object.</param>
/// <param name="pointIndex">Point index.</param>
/// <returns>Y value of the point.</returns>
virtual public double GetYValue(DataPoint point, int pointIndex)
{
double yValue = 0.0;
if( !point.IsEmpty )
{
// Get Y value
yValue = point.YValues[0];
// Adjust point value
if(this._valuePercentages != null &&
this._valuePercentages.Length > pointIndex )
{
yValue = yValue / 100.0 * this._valuePercentages[pointIndex];
}
if(this.Area.AxisY.IsLogarithmic)
{
yValue = Math.Abs(Math.Log( yValue, this.Area.AxisY.LogarithmBase ));
}
else
{
yValue = Math.Abs( yValue );
if(yValue < this._funnelMinPointHeight)
{
yValue = this._funnelMinPointHeight;
}
}
}
return yValue;
}
/// <summary>
/// Helper function, which returns the X value of the point.
/// </summary>
/// <param name="point">Point object.</param>
/// <returns>X value of the point.</returns>
virtual public double GetXValue(DataPoint point)
{
if(this.Area.AxisX.IsLogarithmic)
{
return Math.Abs(Math.Log( point.XValue, this.Area.AxisX.LogarithmBase ));
}
return Math.Abs(point.XValue);
}
/// <summary>
/// Helper function, which returns the Y value of the point.
/// </summary>
/// <param name="common">Chart common elements.</param>
/// <param name="area">Chart area the series belongs to.</param>
/// <param name="series">Sereis of the point.</param>
/// <param name="point">Point object.</param>
/// <param name="pointIndex">Index of the point.</param>
/// <param name="yValueIndex">Index of the Y value to get.</param>
/// <returns>Y value of the point.</returns>
virtual public double GetYValue(
CommonElements common,
ChartArea area,
Series series,
DataPoint point,
int pointIndex,
int yValueIndex)
{
return point.YValues[yValueIndex];
}
#endregion // Y & X values related methods
#region SmartLabelStyle methods
/// <summary>
/// Adds markers position to the list. Used to check SmartLabelStyle overlapping.
/// </summary>
/// <param name="common">Common chart elements.</param>
/// <param name="area">Chart area.</param>
/// <param name="series">Series values to be used.</param>
/// <param name="list">List to add to.</param>
public void AddSmartLabelMarkerPositions(CommonElements common, ChartArea area, Series series, ArrayList list)
{
// Fast Line chart type do not support labels
}
#endregion
#region IDisposable interface implementation
/// <summary>
/// Releases unmanaged and - optionally - managed resources
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
//Nothing to dispose at the base class.
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
/// <summary>
/// PyramidChart class overrides some of the functionality of FunnelChart class.
/// Most of drawing and othere processing is done in the FunnelChart.
/// </summary>
internal class PyramidChart : FunnelChart
{
#region Fields and Constructor
/// <summary>
/// Default constructor
/// </summary>
public PyramidChart()
{
// Renering of the pyramid chart type
base.isPyramid = true;
// Pyramid chart type uses square base by default
base.round3DShape = false;
// Pyramid properties names
base.funnelLabelStyleAttributeName = CustomPropertyName.PyramidLabelStyle;
base.funnelPointGapAttributeName = CustomPropertyName.PyramidPointGap;
base.funnelRotationAngleAttributeName = CustomPropertyName.Pyramid3DRotationAngle;
base.funnelPointMinHeight = CustomPropertyName.PyramidMinPointHeight;
base.funnel3DDrawingStyleAttributeName = CustomPropertyName.Pyramid3DDrawingStyle;
base.funnelInsideLabelAlignmentAttributeName = CustomPropertyName.PyramidInsideLabelAlignment;
base.funnelOutsideLabelPlacementAttributeName = CustomPropertyName.PyramidOutsideLabelPlacement;
}
#endregion
#region IChartType interface implementation
/// <summary>
/// Chart type name
/// </summary>
override public string Name { get{ return ChartTypeNames.Pyramid;}}
#endregion
#region Methods
/// <summary>
/// Gets pyramid data point segment height and width.
/// </summary>
/// <param name="series">Chart type series.</param>
/// <param name="pointIndex">Data point index in the series.</param>
/// <param name="location">Segment top location. Bottom location if reversed drawing order.</param>
/// <param name="height">Returns the height of the segment.</param>
/// <param name="startWidth">Returns top width of the segment.</param>
/// <param name="endWidth">Returns botom width of the segment.</param>
protected override void GetPointWidthAndHeight(
Series series,
int pointIndex,
float location,
out float height,
out float startWidth,
out float endWidth)
{
PointF pointPositionAbs = PointF.Empty;
// Get plotting area position in pixels
RectangleF plotAreaPositionAbs = this.Graph.GetAbsoluteRectangle(this.PlotAreaPosition);
// Calculate total height of plotting area minus reserved space for the gaps
float plotAreaHeightAbs = plotAreaPositionAbs.Height -
this.funnelSegmentGap * (this.pointNumber - ((ShouldDrawFirstPoint()) ? 1 : 2) );
if(plotAreaHeightAbs < 0f)
{
plotAreaHeightAbs = 0f;
}
// Calculate segment height as a part of total Y values in series
height = (float)(plotAreaHeightAbs * (GetYValue(series.Points[pointIndex], pointIndex) / this.yValueTotal));
// Check for minimum segment height
height = CheckMinHeight(height);
// Get intersection point of the horizontal line at the start of the segment
// with the left pre-defined wall of the funnel.
PointF startIntersection = ChartGraphics.GetLinesIntersection(
plotAreaPositionAbs.X, location - height,
plotAreaPositionAbs.Right, location - height,
plotAreaPositionAbs.X, plotAreaPositionAbs.Bottom,
plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f, plotAreaPositionAbs.Y );
if(startIntersection.X > (plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f) )
{
startIntersection.X = plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f;
}
// Get intersection point of the horizontal line at the end of the segment
// with the left pre-defined wall of the funnel.
PointF endIntersection = ChartGraphics.GetLinesIntersection(
plotAreaPositionAbs.X, location,
plotAreaPositionAbs.Right, location,
plotAreaPositionAbs.X, plotAreaPositionAbs.Bottom,
plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f, plotAreaPositionAbs.Y );
if(endIntersection.X > (plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f))
{
endIntersection.X = plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f;
}
// Get segment start and end width
startWidth = (float)Math.Abs( plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f -
startIntersection.X) * 2f;
endWidth = (float)Math.Abs( plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f -
endIntersection.X) * 2f;
// Set point position for annotation anchoring
pointPositionAbs = new PointF(
plotAreaPositionAbs.X + plotAreaPositionAbs.Width / 2f,
location - height / 2f);
// Set pre-calculated point position
series.Points[pointIndex].positionRel = Graph.GetRelativePoint(pointPositionAbs);
}
#endregion // Methods
}
/// <summary>
/// Helper data structure used to store information about single funnel segment.
/// </summary>
internal class FunnelSegmentInfo
{
#region Fields
// ----osiated data point
public DataPoint Point = null;
// Data point index
public int PointIndex = 0;
// Segment top position
public float Location = 0f;
// Segment height
public float Height = 0f;
// Segment top width
public float StartWidth = 0f;
// Segment bottom width
public float EndWidth = 0f;
// Segment has nothing on the top
public bool NothingOnTop = false;
// Segment has nothing on the bottom
public bool NothingOnBottom = false;
#endregion // Fields
}
/// <summary>
/// Helper data structure used to store information about funnel data point label.
/// </summary>
internal class FunnelPointLabelInfo
{
#region Fields
// ----osiated data point
public DataPoint Point = null;
// Data point index
public int PointIndex = 0;
// Label text
public string Text = string.Empty;
// Data point label size
public SizeF Size = SizeF.Empty;
// Position of the data point label
public RectangleF Position = RectangleF.Empty;
// Label style
public FunnelLabelStyle Style = FunnelLabelStyle.OutsideInColumn;
// Inside label vertical alignment
public FunnelLabelVerticalAlignment VerticalAlignment = FunnelLabelVerticalAlignment.Center;
// Outside labels placement
public FunnelLabelPlacement OutsidePlacement = FunnelLabelPlacement.Right;
// Label callout first point
public PointF CalloutPoint1 = PointF.Empty;
// Label callout second point
public PointF CalloutPoint2 = PointF.Empty;
#endregion // Fields
}
}
|