using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Web.UI.DataVisualization.Charting; using System.Collections; namespace System.Web.UI.DataVisualization.Charting.Utilities { /// /// Helper class which improves the readability of the small segments in the Pie chart. /// Pie segments which are too small are shown in a supplemental pie chart series. /// public class PieCollectedDataHelper { #region Fields /// /// Specifies the percentage of the total series values. This value determines /// if the data point value is a "small" value and should be shown as collected. /// public double CollectedPercentage = 5.0; /// /// Position in relative coordinates ( 0,0 - top left corner; 100,100 - bottom right corner) /// where original and supplemental pie charts should be placed. /// public RectangleF ChartAreaPosition = new RectangleF(5f, 5f, 90f, 90f); /// /// Indicates if small segments should be shown as one "collected" segment in the original series /// public bool ShowCollectedDataAsOneSlice = false; /// /// Spacing between the original and supplemental chart areas in percentage /// public float ChartAreaSpacing = 5f; /// /// Size ratio between the original and supplemental chart areas. /// Value of 1.0f indicates that same area size will be used. /// public float SupplementedAreaSizeRatio = 0.9f; /// /// Color of the connection lines /// public Color ConnectionLinesColor = Color.FromArgb(64, 64, 64); /// /// Collected pie segment label /// public string CollectedLabel = "Other"; // Reference to the parameters private Chart chartControl = null; private Series series = null; // Internal use fields private Series supplementalSeries = null; private ChartArea originalChartArea = null; private ChartArea supplementalChartArea = null; private float collectedPieSliceAngle = 0f; #endregion // Fields #region Constructor /// /// Public constructor. /// /// Reference to the chart control. public PieCollectedDataHelper(Chart chartControl) { this.chartControl = chartControl; // Handle chart PostPaint event to draw the "connection" between the // collected pie slice and supplemental chart. this.chartControl.PostPaint +=new EventHandler(this.chart_PostPaint); } #endregion // Constructor #region Methods /// /// Shows small pie segments as supplemental pie chart series in the new chart area. /// /// Series name public void ShowSmallSegmentsAsSupplementalPie(string seriesName) { // Validate input if(this.chartControl == null) { throw(new ArgumentNullException("chartControl")); } if(this.CollectedPercentage > 100.0 || this.CollectedPercentage < 0.0) { throw(new ArgumentException("Value must be in range from 0 to 100 percent.", "CollectedPercentage")); } // Initialize reference to the series this.series = this.chartControl.Series[seriesName]; // Check input series type if(this.series.ChartType != SeriesChartType.Pie && this.series.ChartType != SeriesChartType.Doughnut) { throw(new InvalidOperationException("Only series with Pie or Doughnut chart type can be used.")); } // Check if specified series has data points if(series.Points.Count == 0) { throw(new InvalidOperationException("Cannot perform operatiuon on an empty series.")); } // Create "collected" pie slice in original series this.supplementalChartArea = null; if( CreateCollectedPie() ) { // Calculate width of supplemental chart area float supplementalWidth = (this.ChartAreaPosition.Width - this.ChartAreaSpacing) / 2f * this.SupplementedAreaSizeRatio; // Adjust position of the original chart area this.originalChartArea = this.chartControl.ChartAreas[this.series.ChartArea]; originalChartArea.Position.X = this.ChartAreaPosition.X; originalChartArea.Position.Y = this.ChartAreaPosition.Y; originalChartArea.Position.Width = this.ChartAreaPosition.Width - supplementalWidth - this.ChartAreaSpacing; originalChartArea.Position.Height = this.ChartAreaPosition.Height; // Original chart area must be in 2D mode originalChartArea.Area3DStyle.Enable3D = false; // Create and adjust position of the supplemental chart area this.supplementalChartArea = new ChartArea(); supplementalChartArea.Name = originalChartArea.Name + "_Supplemental"; supplementalChartArea.Position.X = originalChartArea.Position.Right + this.ChartAreaSpacing; supplementalChartArea.Position.Y = this.ChartAreaPosition.Y; supplementalChartArea.Position.Width = supplementalWidth; supplementalChartArea.Position.Height = this.ChartAreaPosition.Height; this.chartControl.ChartAreas.Add(supplementalChartArea); // Create supplemental pie chart series to show all the collected data this.supplementalSeries.Name = this.series.Name + "_Supplemental"; this.supplementalSeries.ChartArea = supplementalChartArea.Name; this.chartControl.Series.Add(supplementalSeries); // Copy some attributes from the original chart area supplementalChartArea.BackColor = originalChartArea.BackColor; supplementalChartArea.BorderColor = originalChartArea.BorderColor; supplementalChartArea.BorderWidth = originalChartArea.BorderWidth; supplementalChartArea.ShadowOffset = originalChartArea.ShadowOffset; // Copy some attributes from the original series this.supplementalSeries.ChartType = this.series.ChartType; this.supplementalSeries.Palette = this.series.Palette; this.supplementalSeries.ShadowOffset = this.series.ShadowOffset; this.supplementalSeries.BorderColor = this.series.BorderColor; this.supplementalSeries.BorderWidth = this.series.BorderWidth; this.supplementalSeries.IsValueShownAsLabel = this.series.IsValueShownAsLabel; this.supplementalSeries.LabelBackColor = this.series.LabelBackColor; this.supplementalSeries.LabelBorderColor = this.series.LabelBorderColor; this.supplementalSeries.LabelBorderWidth = this.series.LabelBorderWidth; this.supplementalSeries.LabelFormat = this.series.LabelFormat; this.supplementalSeries.Font = this.series.Font; } } /// /// Creates the "collected" pie slice data point by re moving and accumulating all /// the values of the data points which values are less then specified percentage. /// /// True if collected pie slice was created. private bool CreateCollectedPie() { // Create supplemental series this.supplementalSeries = new Series(); // Calculate total vale of all point in series double total = 0.0; foreach(DataPoint dataPoint in this.series.Points) { total += Math.Abs(dataPoint.YValues[0]); } // Count how many data points will be presented as collected double minValue = total / 100.0 * this.CollectedPercentage; int collectedPointsCount = 0; for(int index = 0; index < this.series.Points.Count; index++) { double pointValue = Math.Abs(this.series.Points[index].YValues[0]); if(pointValue <= minValue) { ++collectedPointsCount; } } // Do not collect data points if one or less points left in the original series if( (this.series.Points.Count - collectedPointsCount) <= 1 || collectedPointsCount <= 1) { return false; } // Add Collected data point into series before applying palette colors DataPoint colectedDataPoint = null; if(this.ShowCollectedDataAsOneSlice) { colectedDataPoint = new DataPoint(this.series); this.series.Points.Add(colectedDataPoint); } // Apply pallete colors to series to save same data point colors // in supplemental series. this.chartControl.ApplyPaletteColors(); foreach(DataPoint dataPoint in this.series.Points) { // Setting data point color to itself will clear the internal flag which // indicates that point color should be taken from the palette again when // control is rendered next time. dataPoint.Color = dataPoint.Color; } // Remove points which value is less than specified percentage from total double collectedValue = 0.0; for(int index = 0; index < this.series.Points.Count; index++) { double pointValue = Math.Abs(this.series.Points[index].YValues[0]); if(pointValue <= minValue && this.series.Points[index] != colectedDataPoint) { // Add point value to the collected value collectedValue += pointValue; // Add point to supplemental series this.supplementalSeries.Points.Add(this.series.Points[index].Clone()); // Remove point from the series this.series.Points.RemoveAt(index); --index; } } // Add all collected data points at the end of the series if(!ShowCollectedDataAsOneSlice) { foreach(DataPoint dataPoint in this.supplementalSeries.Points) { DataPoint dataPointCollected = dataPoint.Clone(); dataPoint.IsVisibleInLegend = false; this.series.Points.Add(dataPointCollected); // Disable labels in collected slices dataPointCollected.Label = String.Empty; dataPointCollected.LegendText = dataPointCollected.AxisLabel; dataPointCollected.AxisLabel = String.Empty; dataPointCollected.IsValueShownAsLabel = false; } } // Check if we need to add the "collected" data point if(collectedValue > 0.0) { // Set collected data point value and other attributes if(this.ShowCollectedDataAsOneSlice) { colectedDataPoint.YValues[0] = collectedValue; colectedDataPoint.Label = this.CollectedLabel; colectedDataPoint.IsVisibleInLegend = false; // Note: Collected data point may be exploded //colectedDataPoint["Exploded"] = "true"; } // Calculate collected pie slice angle this.collectedPieSliceAngle = (float) ( (360f / 100f) * (collectedValue / (total / 100) ) ); // Adjust the Pie chart start angle, so that the middle of the // collected slice looks directly at 3 o'clock. int startAngle = (int)Math.Round(this.collectedPieSliceAngle / 2.0); this.series["PieStartAngle"] = startAngle.ToString(); return true; } else if(colectedDataPoint != null) { // Remove collected data point this.series.Points.Remove(colectedDataPoint); } return false; } /// /// Chart post paint event handler. /// Used to draw the "connection" lines between the original and supplemental pies. /// /// Event sender. /// Event arguments. private void chart_PostPaint(object sender, System.Web.UI.DataVisualization.Charting.ChartPaintEventArgs e) { if(sender is ChartArea) { ChartArea area = (ChartArea)sender; if(this.supplementalChartArea != null && area.Name == this.supplementalChartArea.Name) { // Get position of the plotting areas in pixels RectangleF originalPosition = GetChartAreaPlottingPosition(this.originalChartArea, e.ChartGraphics); RectangleF supplementalPosition = GetChartAreaPlottingPosition(this.supplementalChartArea, e.ChartGraphics); // Get coordinates of the "connection" lines PointF p1 = GetRotatedPlotAreaPoint(supplementalPosition, 325f); PointF p2 = GetRotatedPlotAreaPoint(supplementalPosition, 215f); PointF p3 = GetRotatedPlotAreaPoint(originalPosition, 90f - this.collectedPieSliceAngle / 2f); PointF p4 = GetRotatedPlotAreaPoint(originalPosition, 90f + this.collectedPieSliceAngle / 2f); // Draw "connection lines" using( Pen pen = new Pen(this.ConnectionLinesColor, 1) ) { e.ChartGraphics.Graphics.DrawLine(pen, p1, p3); e.ChartGraphics.Graphics.DrawLine(pen, p2, p4); } } } } /// /// Helper method which calculates a point on the edje of the pie chart using /// specified angle. /// /// Chart are position in pixels. /// Point angle in degrees. /// Point location in pixels. private PointF GetRotatedPlotAreaPoint(RectangleF areaPosition, float angle) { PointF[] points = new PointF[1]; points[0] = new PointF(areaPosition.X + areaPosition.Width / 2f, areaPosition.Y); using( Matrix transformMatrix = new Matrix() ) { transformMatrix.RotateAt(angle, new PointF( areaPosition.X + areaPosition.Width / 2f, areaPosition.Y + areaPosition.Height / 2f) ); transformMatrix.TransformPoints(points); } return points[0]; } /// /// Helper method which calculates chart area plotting position in pixels. /// /// Chart area to get the plotting area position. /// Chart graphics object. /// Chart area ploting area position in pixels. private RectangleF GetChartAreaPlottingPosition(ChartArea area, ChartGraphics chartGraphics) { RectangleF plottingRect = area.Position.ToRectangleF(); plottingRect.X += (area.Position.Width / 100F) * area.InnerPlotPosition.X; plottingRect.Y += (area.Position.Height / 100F) * area.InnerPlotPosition.Y; plottingRect.Width = (area.Position.Width / 100F) * area.InnerPlotPosition.Width; plottingRect.Height = (area.Position.Height / 100F) * area.InnerPlotPosition.Height; plottingRect = chartGraphics.GetAbsoluteRectangle(plottingRect); return plottingRect; } #endregion // Methods } }