372 lines
14 KiB
C#
372 lines
14 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class PieCollectedDataHelper
|
|
{
|
|
#region Fields
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public double CollectedPercentage = 5.0;
|
|
|
|
/// <summary>
|
|
/// Position in relative coordinates ( 0,0 - top left corner; 100,100 - bottom right corner)
|
|
/// where original and supplemental pie charts should be placed.
|
|
/// </summary>
|
|
public RectangleF ChartAreaPosition = new RectangleF(5f, 5f, 90f, 90f);
|
|
|
|
/// <summary>
|
|
/// Indicates if small segments should be shown as one "collected" segment in the original series
|
|
/// </summary>
|
|
public bool ShowCollectedDataAsOneSlice = false;
|
|
|
|
/// <summary>
|
|
/// Spacing between the original and supplemental chart areas in percentage
|
|
/// </summary>
|
|
public float ChartAreaSpacing = 5f;
|
|
|
|
/// <summary>
|
|
/// Size ratio between the original and supplemental chart areas.
|
|
/// Value of 1.0f indicates that same area size will be used.
|
|
/// </summary>
|
|
public float SupplementedAreaSizeRatio = 0.9f;
|
|
|
|
/// <summary>
|
|
/// Color of the connection lines
|
|
/// </summary>
|
|
public Color ConnectionLinesColor = Color.FromArgb(64, 64, 64);
|
|
|
|
/// <summary>
|
|
/// Collected pie segment label
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Public constructor.
|
|
/// </summary>
|
|
/// <param name="chartControl">Reference to the chart control.</param>
|
|
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<ChartPaintEventArgs>(this.chart_PostPaint);
|
|
}
|
|
|
|
#endregion // Constructor
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// Shows small pie segments as supplemental pie chart series in the new chart area.
|
|
/// </summary>
|
|
/// <param name="seriesName">Series name </param>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <returns>True if collected pie slice was created.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Chart post paint event handler.
|
|
/// Used to draw the "connection" lines between the original and supplemental pies.
|
|
/// </summary>
|
|
/// <param name="sender">Event sender.</param>
|
|
/// <param name="e">Event arguments.</param>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which calculates a point on the edje of the pie chart using
|
|
/// specified angle.
|
|
/// </summary>
|
|
/// <param name="areaPosition">Chart are position in pixels.</param>
|
|
/// <param name="angle">Point angle in degrees.</param>
|
|
/// <returns>Point location in pixels.</returns>
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which calculates chart area plotting position in pixels.
|
|
/// </summary>
|
|
/// <param name="area">Chart area to get the plotting area position.</param>
|
|
/// <param name="chartGraphics">Chart graphics object.</param>
|
|
/// <returns>Chart area ploting area position in pixels.</returns>
|
|
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
|
|
}
|
|
} |