Initial commit for WC-SPC project

This commit is contained in:
meswork
2026-05-27 13:59:56 +08:00
commit 120a18a651
1043 changed files with 255289 additions and 0 deletions

View File

@@ -0,0 +1,623 @@
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>
/// .
/// </summary>
public class ChartDataTableHelper
{
#region Members
protected System.Web.UI.DataVisualization.Charting.Chart ChartObj = null;
protected ArrayList ChartAreas = null;
protected bool AddTableTotals = false;
protected System.Drawing.Color tableColor = Color.White;
protected System.Drawing.Color borderColor = Color.Black;
protected bool enabled = true;
protected bool Initialized = false;
#endregion
#region Properties
/// <summary>
/// Enables or Disables the painting of the Data Table.
/// </summary>
public bool Enabled
{
get
{
return Enabled;
}
set
{
enabled = value;
}
}
/// <summary>
/// Sets or gets the Chart object.
/// </summary>
public System.Web.UI.DataVisualization.Charting.Chart Chart
{
get
{
return ChartObj;
}
set
{
ChartObj = value;
}
}
/// <summary>
/// Sets or gets the Table Color that will be painted.
/// </summary>
public System.Drawing.Color TableColor
{
get
{
return tableColor;
}
set
{
tableColor = value;
}
}
/// <summary>
/// Sets or gets the Table Border Color that will be painted.
/// </summary>
public System.Drawing.Color BorderColor
{
get
{
return borderColor;
}
set
{
borderColor = value;
}
}
#endregion
#region Constructors
/// <summary>
/// Construct a ChartDataTableHelper instance.
/// </summary>
public ChartDataTableHelper()
{
ChartObj = null;
ChartAreas = new ArrayList();
}
/// <summary>
/// Construct a ChartDataTableHelper instance and Initialize all ChartAreas with a table.
/// </summary>
public ChartDataTableHelper(System.Web.UI.DataVisualization.Charting.Chart chartObj)
{
ChartAreas = new ArrayList();
Initialize(chartObj);
}
/// <summary>
/// Construct a ChartDataTableHelper instance and Initialize the specified ChartArea with a table.
/// </summary>
public ChartDataTableHelper(System.Web.UI.DataVisualization.Charting.Chart chartObj, string chartAreaName)
{
ChartAreas = new ArrayList();
Initialize(chartObj, chartAreaName);
}
/// <summary>
/// Construct a ChartDataTableHelper instance, Initialize the specified ChartArea with a table and
/// set a boolean to show or hide total columns.
/// </summary>
public ChartDataTableHelper(System.Web.UI.DataVisualization.Charting.Chart chartObj, string chartAreaName, bool addTableTotals)
{
ChartAreas = new ArrayList();
Initialize(chartObj, chartAreaName, addTableTotals);
}
#endregion
#region Initialization Methods
/// <summary>
/// Initialize all ChartAreas with a table.
/// </summary>
public void Initialize(System.Web.UI.DataVisualization.Charting.Chart chartObj)
{
ChartObj = chartObj;
foreach(ChartArea area in ChartObj.ChartAreas)
{
AddDataTable(area.Name);
}
if(!Initialized)
ChartObj.PostPaint +=new EventHandler<ChartPaintEventArgs>(this.Chart_PostPaint);
Initialized = true;
}
/// <summary>
/// Initialize all ChartAreas with a table and
/// set a boolean to show or hide total columns.
/// </summary>
public void Initialize(System.Web.UI.DataVisualization.Charting.Chart chartObj, bool addTableTotals)
{
AddTableTotals = addTableTotals;
Initialize(chartObj);
}
/// <summary>
/// Initialize the specified ChartArea with a table.
/// </summary>
public void Initialize(System.Web.UI.DataVisualization.Charting.Chart chartObj, string chartAreaName)
{
ChartObj = chartObj;
AddDataTable(chartAreaName);
if(!Initialized)
ChartObj.PostPaint +=new EventHandler<ChartPaintEventArgs>(this.Chart_PostPaint);
Initialized = true;
}
/// <summary>
/// Initialize the specified ChartArea with a table and
/// set a boolean to show or hide total columns.
/// </summary>
public void Initialize(System.Web.UI.DataVisualization.Charting.Chart chartObj, string chartAreaName, bool addTableTotals)
{
ChartObj = chartObj;
AddTableTotals = addTableTotals;
AddDataTable(chartAreaName);
if(!Initialized)
ChartObj.PostPaint +=new EventHandler<ChartPaintEventArgs>(this.Chart_PostPaint);
Initialized = true;
}
/// <summary>
/// Initialize the specified ChartArea with a table.
/// </summary>
public void AddDataTable(string chartAreaName)
{
if(ChartObj == null || ChartAreas.IndexOf(chartAreaName) >= 0)
return;
// add this chart area to the list of chart areas that need to
// have a data table attached
ChartAreas.Add(chartAreaName);
int Row = 0;
if(AddTableTotals)
{
// create a dummy series that will not be shown but is used for
// showing the totals in the data table
ChartObj.Series.Add("DUMMY");
ChartObj.Series["DUMMY"].ChartArea = chartAreaName;
ChartObj.Series["DUMMY"].Enabled = false;
ChartObj.Series["DUMMY"].Color = Color.Gainsboro;
}
// for each of the series that are attached to this
// named chart area, create a custom axis label.
// All tables lines will be drawn on a paint event.
// ****************************************************
// NOTE: ALL SERIES MUST HAVE THE SAME NUMBER OF POINTS!
// ****************************************************
foreach(Series ser in ChartObj.Series)
{
if(chartAreaName == ser.ChartArea)
{
if(AddTableTotals)
{
// shadows must be turned off otherwise
// they will still show up for the transparent points
ser.ShadowOffset = 0;
// adjust the series values to ensure they are not
// indexed and they are sorted... plus adding each point
// to make the dummy series data
AdjustXValues(ser);
}
Row++;
double From = 0.0;
double To = 0.0;
bool firstPoint = true;
double YValueTotal = 0;
foreach(DataPoint dp in ser.Points)
{
if(AddTableTotals)
YValueTotal += dp.YValues[0];
if(firstPoint && dp.XValue == 0)
From = 0.5;
else if(firstPoint)
From = dp.XValue - 0.5;
if(firstPoint)
{
ChartObj.ChartAreas[chartAreaName].AxisX.Minimum = From;
ChartObj.ChartAreas[chartAreaName].AxisX.MajorGrid.Interval = 1;
ChartObj.ChartAreas[chartAreaName].AxisX.MajorTickMark.Interval = 1;
ChartObj.ChartAreas[chartAreaName].AxisX.LabelStyle.Interval = 1;
ChartObj.ChartAreas[chartAreaName].AxisX.MajorGrid.IntervalOffset = 0.5;
ChartObj.ChartAreas[chartAreaName].AxisX.MajorTickMark.IntervalOffset = 0.5;
ChartObj.ChartAreas[chartAreaName].AxisX.LabelStyle.IntervalOffset = 0.5;
}
To = From + 1;
ChartObj.ChartAreas[chartAreaName].AxisX.CustomLabels.Add(
From, To,
" ", // space used as a placeholder
Row, LabelMarkStyle.None, GridTickTypes.None
);
firstPoint = false;
From += 1;
}
if(AddTableTotals)
ser.Points[ser.Points.Count-1].YValues[0] = YValueTotal;
ChartObj.ChartAreas[chartAreaName].AxisX.Maximum = To;
}
}
if(AddTableTotals)
AdjustYMaximum(chartAreaName);
}
/// <summary>
/// With the addition of Totals, the chart will try to set the maximum
/// values according to these totals. This will cause some series to be
/// barely visible. Since the points are transparent this is a poor behavior.
/// This method will find and explicitly set the YAxis maximum to something
/// a little more with the user expectations.
/// </summary>
private void AdjustYMaximum(string chartAreaName)
{
double MaxYValue = 0;
// find the max YValue from all points in all series
foreach(Series ser in ChartObj.Series)
{
if(chartAreaName == ser.ChartArea && ser.Enabled)
{
// check agains all points except the last point
// which is the totals column
for(int index = 0; index < ser.Points.Count-1; index++)
{
DataPoint pt = ser.Points[index];
if(pt.YValues[0] > MaxYValue)
MaxYValue = pt.YValues[0];
}
}
}
double LogValue = (int)(Math.Log10(MaxYValue)) + 1;
double NewMaxYValue = Math.Pow(10, LogValue);
double ratio = MaxYValue / NewMaxYValue;
double divisor = 1;
if(ratio <= 0.1)
divisor = 10;
else if(ratio < 0.2)
divisor = 5;
else if(ratio < 0.25)
divisor = 4;
else if(ratio < 0.4)
divisor = 2.5;
else if(ratio < 0.5)
divisor = 2;
else if(ratio < 0.8)
divisor = 1.25;
ChartObj.ChartAreas[chartAreaName].AxisY.Maximum = NewMaxYValue / divisor;
ChartObj.ChartAreas[chartAreaName].AxisY.RoundAxisValues();
}
/// <summary>
/// A cleanup method that ensures the XValues are sorted accordingly and set explicitly.
/// It will also create the totals for the DUMMY series.
/// </summary>
private void AdjustXValues(System.Web.UI.DataVisualization.Charting.Series series)
{
bool AddDummyPoints = true;
if(series.Name == "DUMMY")
return;
else if(ChartObj.Series["DUMMY"].Points.Count > 0)
AddDummyPoints = false;
// sort the series
series.Sort(PointSortOrder.Ascending, "X");
bool IsIndexed = false;
if(series.IsXValueIndexed)
IsIndexed = true;
else
{
bool IsFirstPoint = true;
bool IsLastPointZero = false;
// the series X values must be set and greater than zero
foreach(DataPoint pt in series.Points)
{
if(pt.XValue == 0 && !IsFirstPoint && IsLastPointZero)
{
IsIndexed = true;
break;
}
else if (pt.XValue == 0 && IsFirstPoint)
IsLastPointZero = true;
IsFirstPoint = false;
}
}
if(IsIndexed)
{
series.IsXValueIndexed = false;
int XValue = 0;
foreach(DataPoint pt in series.Points)
pt.XValue = ++XValue;
}
series.Points.AddXY(series.Points[series.Points.Count-1].XValue + 1, 0);
series.Points[series.Points.Count-1].AxisLabel = "Total";
series.Points[series.Points.Count-1].Color = Color.Transparent;
series.Points[series.Points.Count-1].BorderColor = Color.Transparent;
int index = 0;
foreach(DataPoint pt in series.Points)
{
if(AddDummyPoints)
{
ChartObj.Series["DUMMY"].Points.AddXY(pt.XValue, pt.YValues[0]);
}
else
{
ChartObj.Series["DUMMY"].Points[index].YValues[0] += pt.YValues[0];
}
index++;
}
}
#endregion
#region Remove Table
public void RemoveDataTable(string chartAreaName)
{
if (ChartObj.ChartAreas.IndexOf(chartAreaName) >= 0)
ChartObj.ChartAreas[chartAreaName].AxisX.CustomLabels.Clear();
if(ChartAreas.IndexOf(chartAreaName) >= 0)
{
ChartAreas.RemoveAt(ChartAreas.IndexOf(chartAreaName));
}
}
#endregion
#region Paint Event Handling
/// <summary>
/// Chart Paint event handler.
/// </summary>
private void Chart_PostPaint(object sender, System.Web.UI.DataVisualization.Charting.ChartPaintEventArgs e)
{
if( e.ChartElement is ChartArea )
{
ChartArea area = (ChartArea)e.ChartElement;
// call the paint method.
if(ChartAreas.IndexOf(area.Name) >= 0 && enabled)
{
PaintDataTable(sender, e);
}
}
}
/// <summary>
/// This method does all the work for the painting of the data table.
/// </summary>
private void PaintDataTable(object sender, System.Web.UI.DataVisualization.Charting.ChartPaintEventArgs e)
{
ChartArea area = (ChartArea)e.ChartElement;
// get the rect of the chart area
RectangleF rect = e.ChartGraphics.GetAbsoluteRectangle( area.Position.ToRectangleF() );
// get the inner plot position
ElementPosition elemPos = area.InnerPlotPosition;
// find the coordinates of the inner plot position
float x = rect.X + (rect.Width / 100 * elemPos.X);
float y = rect.Y + (rect.Height / 100 * elemPos.Y);
float ChartAreaBottomY = rect.Y + rect.Height;
float width = (rect.Width / 100 * elemPos.Width);
float height = (rect.Height / 100 * elemPos.Height);
// find the height of the font that will be used
Font axisFont = area.AxisX.LabelStyle.Font;
string testString = "ForFontHeight";
SizeF axisFontSize = e.ChartGraphics.Graphics.MeasureString(testString, axisFont);
// find the height of the font that will be used
Font titleFont = area.AxisX.TitleFont;
testString = area.AxisX.Title;
SizeF titleFontSize = e.ChartGraphics.Graphics.MeasureString(testString, titleFont);
int seriesCount = 0;
// for each series that is attached to the chart area,
// draw some boxes around the labels in the color provided
for(int i = e.Chart.Series.Count-1; i >= 0; i--)
{
if(area.Name == e.Chart.Series[i].ChartArea)
{
seriesCount++;
}
}
// now, if a box was actually drawn, then draw
// the verticle lines to separate the columns of the table.
if(seriesCount > 0)
{
for(int i = 0; i < e.Chart.Series.Count; i++)
{
if(area.Name == e.Chart.Series[i].ChartArea)
{
double min = area.AxisX.Minimum;
double max = area.AxisX.Maximum;
// modify the min value for the current axis view
if(area.AxisX.ScaleView.Position-1 > min)
min = area.AxisX.ScaleView.Position-1;
// modify the max value for the currect axis view
if( (area.AxisX.ScaleView.Position + area.AxisX.ScaleView.Size + 0.5) < max)
max = area.AxisX.ScaleView.Position + area.AxisX.ScaleView.Size + 0.5;
// find the starting point that will be display.
// this is dependent on the current axis view.
// this sample assumes the same number of points in each
// series so always take from the zeroth series
int pointIndex = 0;
foreach(DataPoint pt in ChartObj.Series[0].Points)
{
if(pt.XValue > min)
break;
pointIndex++;
}
bool TableLegendDrawn = false;
for(double AxisValue = min; AxisValue < max; AxisValue++)
{
float pixelX = (float)e.ChartGraphics.GetPositionFromAxis(area.Name, AxisName.X, AxisValue);
float nextPixelX = (float)e.ChartGraphics.GetPositionFromAxis(area.Name, AxisName.X, AxisValue + 1);
float pixelY = ChartAreaBottomY - titleFontSize.Height - (seriesCount * axisFontSize.Height);
PointF point1 = PointF.Empty;
PointF point2 = PointF.Empty;
// Set Maximum and minimum points
point1.X = pixelX;
point1.Y = 0;
// Convert relative coordinates to absolute coordinates.
point1 = e.ChartGraphics.GetAbsolutePoint(point1);
point2.X = point1.X;
point2.Y = ChartAreaBottomY - titleFontSize.Height;
point1.Y = pixelY;
// Draw connection line
e.ChartGraphics.Graphics.DrawLine(new Pen(borderColor), point1,point2);
point2.X = nextPixelX;
point2.Y = 0;
point2 = e.ChartGraphics.GetAbsolutePoint(point2);
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
// for each series draw one value in the column
int row = 0;
foreach(Series ser in ChartObj.Series)
{
if(area.Name == ser.ChartArea)
{
if(!TableLegendDrawn)
{
// draw the series color box
e.ChartGraphics.Graphics.FillRectangle(new SolidBrush(ser.Color),
x-10, row*(axisFont.Height)+(point1.Y), 10, axisFontSize.Height);
e.ChartGraphics.Graphics.DrawRectangle(new Pen(borderColor),
x-10, row*(axisFont.Height)+(point1.Y), 10, axisFontSize.Height);
e.ChartGraphics.Graphics.FillRectangle(new SolidBrush(tableColor),
x,
row*(axisFont.Height)+(point1.Y),
width,
axisFontSize.Height);
e.ChartGraphics.Graphics.DrawRectangle(new Pen(borderColor),
x,
row*(axisFont.Height)+(point1.Y),
width,
axisFontSize.Height);
}
if(pointIndex < ser.Points.Count)
{
string label = ser.Points[pointIndex].YValues[0].ToString();
RectangleF textRect = new RectangleF(point1.X, row*(axisFont.Height)+(point1.Y+1), point2.X-point1.X, axisFont.Height);
e.ChartGraphics.Graphics.DrawString(label, axisFont, new SolidBrush(area.AxisX.LabelStyle.ForeColor), textRect, format);
}
row++;
}
}
TableLegendDrawn = true;
pointIndex++;
}
// do this only once so break!
break;
}
}
}
}
#endregion
}
}

View File

@@ -0,0 +1,258 @@
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 that creates a histogram chart. Histogram is a data
/// distribution chart which shows how many values, from the data series,
/// are inside each segment interval.
///
/// You can define how many intervals you want to have using the SegmentIntervalNumber
/// field or the exact length of the interval using the SegmentIntervalWidth
/// field. Actual segment interval number can be slightly different due
/// to the automatic interval rounding.
/// </summary>
public class HistogramChartHelper
{
#region Fields
/// <summary>
/// Number of class intervals the data range is devided in.
/// This property only has affect when "SegmentIntervalWidth" is
/// set to double.NaN.
/// </summary>
public int SegmentIntervalNumber = 20;
/// <summary>
/// Histogram class interval width. Setting this value to "double.NaN"
/// will result in automatic width calculation based on the data range
/// and number of required interval specified in "SegmentIntervalNumber".
/// </summary>
public double SegmentIntervalWidth = double.NaN;
/// <summary>
/// Indicates that percent frequency should be shown on the right axis
/// </summary>
public bool ShowPercentOnSecondaryYAxis = true;
#endregion // Fields
#region Methods
/// <summary>
/// Creates a histogram chart.
/// </summary>
/// <param name="chartControl">Chart control reference.</param>
/// <param name="dataSeriesName">Name of the series which stores the original data.</param>
/// <param name="histogramSeriesName">Name of the histogram series.</param>
public void CreateHistogram(
Chart chartControl,
string dataSeriesName,
string histogramSeriesName)
{
// Validate input
if (chartControl == null)
{
throw (new ArgumentNullException("chartControl"));
}
if (chartControl.Series.IndexOf(dataSeriesName) < 0)
{
throw (new ArgumentException("Series with name'" + dataSeriesName + "' was not found.", "dataSeriesName"));
}
// Make data series invisible
chartControl.Series[dataSeriesName].Enabled = false;
// Check if histogram series exsists
Series histogramSeries = null;
if (chartControl.Series.IndexOf(histogramSeriesName) < 0)
{
// Add new series
histogramSeries = chartControl.Series.Add(histogramSeriesName);
// Set new series chart type and other attributes
histogramSeries.ChartType = SeriesChartType.Column;
histogramSeries.BorderColor = Color.Black;
histogramSeries.BorderWidth = 1;
histogramSeries.BorderDashStyle = ChartDashStyle.Solid;
}
else
{
histogramSeries = chartControl.Series[histogramSeriesName];
histogramSeries.Points.Clear();
}
// Get data series minimum and maximum values
double minValue = double.MaxValue;
double maxValue = double.MinValue;
int pointCount = 0;
foreach (DataPoint dataPoint in chartControl.Series[dataSeriesName].Points)
{
// Process only non-empty data points
if (!dataPoint.IsEmpty)
{
if (dataPoint.YValues[0] > maxValue)
{
maxValue = dataPoint.YValues[0];
}
if (dataPoint.YValues[0] < minValue)
{
minValue = dataPoint.YValues[0];
}
++pointCount;
}
}
// Calculate interval width if it's not set
if (double.IsNaN(this.SegmentIntervalWidth))
{
this.SegmentIntervalWidth = (maxValue - minValue) / SegmentIntervalNumber;
this.SegmentIntervalWidth = RoundInterval(this.SegmentIntervalWidth);
}
// Round minimum and maximum values
minValue = Math.Floor(minValue / this.SegmentIntervalWidth) * this.SegmentIntervalWidth;
maxValue = Math.Ceiling(maxValue / this.SegmentIntervalWidth) * this.SegmentIntervalWidth;
// Create histogram series points
double currentPosition = minValue;
for (currentPosition = minValue; currentPosition <= maxValue; currentPosition += this.SegmentIntervalWidth)
{
// Count all points from data series that are in current interval
int count = 0;
foreach (DataPoint dataPoint in chartControl.Series[dataSeriesName].Points)
{
if (!dataPoint.IsEmpty)
{
double endPosition = currentPosition + this.SegmentIntervalWidth;
if (dataPoint.YValues[0] >= currentPosition &&
dataPoint.YValues[0] < endPosition)
{
++count;
}
// Last segment includes point values on both segment boundaries
else if (endPosition >= maxValue)
{
if (dataPoint.YValues[0] >= currentPosition &&
dataPoint.YValues[0] <= endPosition)
{
++count;
}
}
}
}
// Add data point into the histogram series
histogramSeries.Points.AddXY(currentPosition + this.SegmentIntervalWidth / 2.0, count);
//histogramSeries.Points.AddXY("", count);
//histogramSeries.Points.AddY( count);
}
// Adjust series attributes
histogramSeries["PointWidth"] = "1";
// Adjust chart area
ChartArea chartArea = chartControl.ChartAreas[histogramSeries.ChartArea];
chartArea.AxisY.Title = "ƵÊý";
chartArea.AxisX.Minimum = minValue;
chartArea.AxisX.Maximum = maxValue;
// Set axis interval based on the histogram class interval
// and do not allow more than 10 labels on the axis.
double axisInterval = this.SegmentIntervalWidth;
while ((maxValue - minValue) / axisInterval > 10.0)
{
axisInterval *= 2.0;
}
chartArea.AxisX.Interval = axisInterval;
// Set chart area secondary Y axis
chartArea.AxisY2.Enabled = AxisEnabled.Auto;
if (this.ShowPercentOnSecondaryYAxis)
{
chartArea.RecalculateAxesScale();
chartArea.AxisY2.Enabled = AxisEnabled.True;
chartArea.AxisY2.LabelStyle.Format = "P0";
chartArea.AxisY2.MajorGrid.Enabled = false;
chartArea.AxisY2.Title = "Percent of Total";
chartArea.AxisY2.Minimum = 0;
chartArea.AxisY2.Maximum = chartArea.AxisY.Maximum / (pointCount / 100.0);
double minStep = (chartArea.AxisY2.Maximum > 20.0) ? 5.0 : 1.0;
chartArea.AxisY2.Interval = Math.Ceiling((chartArea.AxisY2.Maximum / 5.0 / minStep)) * minStep;
}
}
/// <summary>
/// Helper method which rounds specified axsi interval.
/// </summary>
/// <param name="interval">Calculated axis interval.</param>
/// <returns>Rounded axis interval.</returns>
public double RoundInterval( double interval )
{
// If the interval is zero return error
if( interval == 0.0 )
{
throw( new ArgumentOutOfRangeException("interval", "Interval can not be zero."));
}
// If the real interval is > 1.0
double step = -1;
double tempValue = interval;
while( tempValue > 1.0 )
{
step ++;
tempValue = tempValue / 10.0;
if( step > 1000 )
{
throw( new InvalidOperationException( "Auto interval error due to invalid point values or axis minimum/maximum." ) );
}
}
// If the real interval is < 1.0
tempValue = interval;
if( tempValue < 1.0 )
{
step = 0;
}
while( tempValue < 1.0 )
{
step --;
tempValue = tempValue * 10.0;
if( step < -1000 )
{
throw( new InvalidOperationException( "Auto interval error due to invalid point values or axis minimum/maximum." ) );
}
}
double tempDiff = interval / Math.Pow( 10.0, step );
if( tempDiff < 3.0 )
{
tempDiff = 2.0;
}
else if( tempDiff < 7.0 )
{
tempDiff = 5.0;
}
else
{
tempDiff = 10.0;
}
// Make a correction of the real interval
return tempDiff * Math.Pow( 10.0, step );
}
#endregion // Methods
}
}

View File

@@ -0,0 +1,326 @@
//=================================================================
// File: FFT.cs
//
// Namespace: System.Web.UI.DataVisualization.Charting.Utilities
//
// Classes: FFT
//
// Purpose: Used for the fast fourier transformation algorithm
//
//===================================================================
// Chart Control for ASP.Net
//===================================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace System.Web.UI.DataVisualization.Charting.Utilities
{
/// <summary>
/// Helper class which implements the various window functions for determination of the filter
/// coefficients.
/// </summary>
class FFT
{
#region Members
/// <summary>
/// Filter type enumeration for identification of what type of filter we want coefficients
/// for.
/// </summary>
public enum FilterType { HighPass, LowPass, BandPass };
/// <summary>
/// Algorithm enumeration for choice of algorithm
/// </summary>
public enum Algorithm { Kaiser, Hann, Hamming, Blackman, Rectangular };
private float myRate;
private float myFreqFrom;
private float myFreqTo;
private float myAttenuation;
private float myBand;
private float myAlpha;
private int myOrder;
/// <summary>
/// Shannon sampling frequency
/// </summary>
private float myFS;
#endregion
#region Properties
/// <summary>
/// Sampling rate
/// </summary>
public float Rate
{
get { return myRate; }
set
{
myRate = value;
myFS = 0.5f * myRate;
}
}
/// <summary>
/// Starting frequency for passband. Must be lower than the ending frequency.
/// </summary>
public float FreqFrom
{
get { return myFreqFrom; }
set { myFreqFrom = value; }
}
/// <summary>
/// Ending frequency for passband. Must be higher than the starting frequency.
/// </summary>
public float FreqTo
{
get { return myFreqTo; }
set { myFreqTo = value; }
}
/// <summary>
/// Stopband attenuation.
/// </summary>
public float StopBandAttenuation
{
get { return myAttenuation; }
set { myAttenuation = value; }
}
/// <summary>
/// Transition band.
/// </summary>
public float TransitionBand
{
get { return myBand; }
set { myBand = value; }
}
/// <summary>
/// Alpha value used for the Kaiser algorithm.
/// </summary>
public float Alpha
{
get { return myAlpha; }
set { myAlpha = value; }
}
/// <summary>
/// Filter order. Must be an even number.
/// </summary>
public int Order
{
get { return myOrder; }
set { myOrder = value; }
}
#endregion
#region Constructors
/// <summary>
/// Construct a FFT instance and initialize with default values.
/// </summary>
public FFT()
{
//default rate to 8000
Rate = 8000;
//default attenuation to 60db
this.myAttenuation = 60;
//default transition band to 500hz
this.myBand = 500;
//default order to 0 so that we'll know if it was changed by the user or not
this.myOrder = 0;
//default Alpha to 4
this.myAlpha = 4;
}
#endregion
#region Mathematical Functions
/// <summary>
/// Bessel is the zeroth order Bessel function which is used in the Kaiser window.
/// This is a polynomial approximation of the zeroth order modified Bessel function found in:
/// W.H. Press, B.P. Flannery, S.A. Teukolsky, and W.T. Vetterling.
/// Numerical Recipes in C: The Art of Scientific Computing.
/// Cambridge UP, 1988.
/// P. 237
/// </summary>
/// <param name="x">Input number which the Bessel will be performed on</param>
private float Bessel(float x)
{
double ax, ans;
double y;
ax = System.Math.Abs(x);
if (ax < 3.75)
{
y = x / 3.75;
y *= y;
ans = 1.0 + y * (3.5156229 + y * (3.0899424 + y * (1.2067492
+ y * (0.2659732 + y * (0.360768e-1 + y * 0.45813e-2)))));
}
else
{
y = 3.75 / ax;
ans = (System.Math.Exp(ax) / System.Math.Sqrt(ax)) * (0.39894228 + y * (0.1328592e-1
+ y * (0.225319e-2 + y * (-0.157565e-2 + y * (0.916281e-2
+ y * (-0.2057706e-1 + y * (0.2635537e-1 + y * (-0.1647633e-1
+ y * 0.392377e-2))))))));
}
return (float)ans;
}
#endregion
#region Generate Coefficients
/// <summary>
/// Calculate the coefficients to be used by the filter function.
/// </summary>
/// <param name="filterType">Enum type which specifies the filter to be performed.</param>
/// <param name="alg">Enum type which specifies which algorithm to be used for the window
/// algorithm.</param>
public float[] GenerateCoefficients(FilterType filterType, Algorithm alg)
{
//Calculate order if it hasn't been set
if (this.myOrder == 0)
this.myOrder = (int)(((this.myAttenuation - 7.95f) / (this.myBand * 14.36f / this.myFS) + 1.0f) * 2.0f) - 1;
float[] window = new float[(this.myOrder / 2) + 1];
float[] coEff = new float[this.myOrder + 1];
float ps;
float pe;
const float PI = (float)System.Math.PI;
int o2 = this.myOrder / 2;
//Switch based on algorithm
switch (alg)
{
case Algorithm.Kaiser:
//Kaiser Window function
for (int i = 1; i <= o2; i++)
{
window[i] = Bessel(this.myAlpha * (float)System.Math.Sqrt(1.0f - (float)System.Math.Pow((float)i / o2, 2))) / Bessel(this.myAlpha);
}
//Stopband attenuation and transition band should be set by the user
break;
case Algorithm.Hann:
//Hann window function
for (int i = 1; i <= o2; i++)
{
window[i] = 0.5f + 0.5f * (float)System.Math.Cos((PI / (o2 + 1)) * i);
}
//Set the min stopband attenuation
this.StopBandAttenuation = 44.0f;
//Set the transition band
this.TransitionBand = 6.22f * this.myFS / this.myOrder;
break;
case Algorithm.Hamming:
//Hamming window function
for (int i = 1; i <= o2; i++)
{
window[i] = 0.54f + 0.46f * (float)System.Math.Cos((PI / o2) * i);
}
//Set the min stopband attenuation
this.StopBandAttenuation = 53.0f;
//Set the transition band
this.TransitionBand = 6.64f * this.myFS / this.myOrder;
break;
case Algorithm.Blackman:
//Blackman window function
for (int i = 1; i <= o2; i++)
{
window[i] = 0.42f + 0.5f * (float)Math.Cos((PI / o2) * i) + 0.08f * (float)Math.Cos(2.0f * (PI / o2) * i);
}
//Set the min stopband attenuation
this.StopBandAttenuation = 74.0f;
//Set the transition band
this.TransitionBand = 11.13f * this.myFS / this.myOrder;
break;
case Algorithm.Rectangular:
//Rectangular window function
for (int i = 1; i <= o2; i++)
{
window[i] = 1.0f;
}
//Set the min stopband attenuation
this.StopBandAttenuation = 21.0f;
//Set the transition band
this.TransitionBand = 1.84f * this.myFS / this.myOrder;
break;
default:
//Zero all values if nothing was set (error)
for (int i = 1; i <= o2; i++)
{
window[i] = 0.0f;
}
break;
}
//Switch based on filtertype
switch (filterType)
{
case FilterType.BandPass:
pe = PI / 2 * (this.FreqTo - this.FreqFrom + this.myBand) / this.myFS;
ps = PI / 2 * (this.FreqFrom + this.FreqTo) / this.myFS;
break;
case FilterType.LowPass:
pe = PI * (this.FreqTo + this.myBand / 2) / this.myFS;
ps = 0.0f;
break;
case FilterType.HighPass:
pe = PI * (1.0f - (this.FreqFrom - this.myBand / 2) / this.myFS);
ps = PI;
break;
default:
pe = 0.0f;
ps = 0.0f;
break;
}
//Set first coefficient value
coEff[0] = pe / PI;
//Calculate coefficientsw
for (int i = 1; i <= o2; i++)
{
coEff[i] = window[i] * (float)System.Math.Sin(i * pe) * (float)System.Math.Cos(i * ps) / (i * PI);
}
//Shift Impulse
for (int i = o2 + 1; i <= this.myOrder; i++)
{
coEff[i] = coEff[i - o2];
}
for (int i = 0; i <= o2 - 1; i++)
{
coEff[i] = coEff[this.myOrder - i];
}
coEff[o2] = pe / PI;
return coEff;
}
#endregion
}
}

View File

@@ -0,0 +1,324 @@
//=================================================================
// File: FIRFilters.cs
//
// Namespace: System.Web.UI.DataVisualization.Charting.Utilities
//
// Classes: FIRFilters, FFT
//
// Purpose: Used to perform digital filters on charts
//
//===================================================================
// Chart Control for ASP.Net
//===================================================================
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.DataVisualization.Charting;
namespace System.Web.UI.DataVisualization.Charting.Utilities
{
/// <summary>
/// Helper class which implements the filtering functions. Currently Low Pass, High Pass and
/// Band Pass are implemented.
/// </summary>
class FIRFilters
{
#region Members
/// <summary>
/// The number of samples is the same as the number of points
/// </summary>
private int mySamples;
/// <summary>
/// Holds the coefficient from the window function
/// </summary>
private float[] myCoeff;
/// <summary>
/// Holds the series which has the input data
/// </summary>
private Series myInputSeries;
/// <summary>
/// Holds the series which we are outputting to
/// </summary>
private Series myFilterSeries;
/// <summary>
/// FFT algorithm object
/// </summary>
private FFT myFFT;
/// <summary>
/// Holds the current algorithm selected. Enumeration type is drawn from the FFT object.
/// </summary>
public FFT.Algorithm CurrentAlgorithm;
private float myFreqFrom;
private float myFreqTo;
private float myAttenuation;
private float myBand;
private float myAlpha;
private int myTaps;
private int myOrder;
#endregion
#region Properties
/// <summary>
/// The starting passband frequency, must be lower than ending frequency.
/// </summary>
public float FreqFrom
{
get { return myFreqFrom; }
set { myFreqFrom = value; }
}
/// <summary>
/// The ending passband frequency, must be higher than starting frequency.
/// </summary>
public float FreqTo
{
get { return myFreqTo; }
set { myFreqTo = value; }
}
/// <summary>
/// Stopband attenuation
/// </summary>
public float StopBandAttenuation
{
get { return myAttenuation; }
set {
myAttenuation = value;
this.myFFT.StopBandAttenuation = myAttenuation;
}
}
/// <summary>
/// Transition band
/// </summary>
public float TransitionBand
{
get { return myBand; }
set {
myBand = value;
this.myFFT.TransitionBand = myBand;
}
}
/// <summary>
/// Alpha value used for the Kaiser algorithm.
/// </summary>
public float Alpha
{
get { return myAlpha; }
set {
myAlpha = value;
this.myFFT.Alpha = myAlpha;
}
}
/// <summary>
/// Number of taps to be used. Taps is the number of samples processed at any one time.
/// </summary>
public int Taps
{
get { return myTaps; }
set { myTaps = value; }
}
/// <summary>
/// Filter order. Must be an even number.
/// </summary>
public int Order
{
get { return myOrder; }
set
{
//Assure value is even
if ((value % 2) == 0)
{
myOrder = value;
this.myFFT.Order = myOrder;
}
else
throw new ArgumentOutOfRangeException("Order", "Filter order must be an even number.");
}
}
#endregion
#region Constructors
/// <summary>
/// Main constructor. Resets all settings within the FFT algorithm object.
/// </summary>
public FIRFilters()
{
//Create a new FFT object
this.myFFT = new FFT();
//Default algorithm to Kaiser
this.CurrentAlgorithm = FFT.Algorithm.Kaiser;
//Default taps to 35
this.myTaps = 35;
}
#endregion
#region Methods
/// <summary>
/// Performs a low pass filter. Output series will be cleared before being
/// output to. If passband start and end frequencies are left at 0, defaults are used.
/// </summary>
/// <param name="iseries">Input series that contains input data. Input Y-Values must be contained in YValues[0] or unexpected output will occur</param>
/// <param name="oseries">Output series to which filter will be written. Output Y-Values are written to YValues[0]</param>
public void LowPassFilter(Series iseries, Series oseries)
{
//If no start and end frequencies are specified, default low pass frequency range to:
//0 - 1000hz
if (this.myFreqFrom == 0.0f && this.myFreqTo == 0.0f)
{
this.myFFT.FreqFrom = 0.0f;
this.myFFT.FreqTo = 1000.0f;
}
else
{
this.myFFT.FreqFrom = this.myFreqFrom;
this.myFFT.FreqTo = this.myFreqTo;
}
//Generate the actual coefficients
this.myCoeff = this.myFFT.GenerateCoefficients(FFT.FilterType.LowPass, CurrentAlgorithm);
//Filter the series based on the coefficients generated
Filter(iseries, oseries);
}
/// <summary>
/// Performs a high pass filter. Output series will be cleared before being
/// output to. If passband start and end frequencies are left at 0, defaults are used.
/// </summary>
/// <param name="iseries">Input series that contains input data. Input Y-Values must be contained in YValues[0] or unexpected output will occur</param>
/// <param name="oseries">Output series to which filter will be written. Output Y-Values are written to YValues[0]</param>
public void HighPassFilter(Series iseries, Series oseries)
{
//If no start and end frequencies are specified, default high pass frequency range to:
//2000 - 4000hz
if (this.myFreqFrom == 0.0f && this.myFreqTo == 0.0f)
{
this.myFFT.FreqFrom = 2000.0f;
this.myFFT.FreqTo = 4000.0f;
}
else
{
this.myFFT.FreqFrom = this.myFreqFrom;
this.myFFT.FreqTo = this.myFreqTo;
}
//Generate the actual coefficients
this.myCoeff = this.myFFT.GenerateCoefficients(FFT.FilterType.HighPass, CurrentAlgorithm);
//Filter the series based on the coefficients generated
Filter(iseries, oseries);
}
/// <summary>
/// Performs a band pass filter. Output series will be cleared before being
/// output to. If passband start and end frequencies are left at 0, defaults are used.
/// </summary>
/// <param name="iseries">Input series that contains input data. Input Y-Values must be contained in YValues[0] or unexpected output will occur</param>
/// <param name="oseries">Output series to which filter will be written. Output Y-Values are written to YValues[0]</param>
public void BandPassFilter(Series iseries, Series oseries)
{
//If no start and end frequencies are specified, default band pass frequency range to:
//1000 - 1000hz
if (this.myFreqFrom == 0.0f && this.myFreqTo == 0.0f)
{
this.myFFT.FreqFrom = 1000.0f;
this.myFFT.FreqTo = 1000.0f;
}
else
{
this.myFFT.FreqFrom = this.myFreqFrom;
this.myFFT.FreqTo = this.myFreqTo;
}
//Generate the actual coefficients
this.myCoeff = this.myFFT.GenerateCoefficients(FFT.FilterType.BandPass, CurrentAlgorithm);
//Filter the series based on the coefficients generated
Filter(iseries, oseries);
}
#endregion
#region Initialization
/// <summary>
/// Initializes the FIRFilters object by setting the input and output series members for use
/// by the filter.
/// </summary>
/// <param name="iseries">Input series that contains input data</param>
/// <param name="oseries">Output series to which filter will be written</param>
private void SetIOSeries(Series iseries, Series oseries)
{
this.myInputSeries = iseries;
this.myFilterSeries = oseries;
//Samples is the number of points contained in the input
this.mySamples = myInputSeries.Points.Count;
}
#endregion
#region Filter
/// <summary>
/// Performs the actual filter. Coefficients should have already be generated by the calling
/// function, this function merely applies them and physically adds the points to the output series.
/// </summary>
/// <param name="iseries">Input series that contains input data</param>
/// <param name="oseries">Output series to which filter will be written</param>
private void Filter(Series iseries, Series oseries)
{
float[] x = new float[myTaps];
float y;
//Set the series
SetIOSeries(iseries, oseries);
//Clear series
myFilterSeries.Points.Clear();
//Initialize x
for (int i = 1; i < myTaps; i++)
x[i] = 0.0f;
//Loop through every data point
for (int i = 0; i < mySamples; i++)
{
//Initialize y
y = 0.0f;
//Obtain the data value (Y value) at the specified X value (i)
x[0] = Convert.ToSingle(myInputSeries.Points[i].YValues[0]);
//Loop through from 0 to number of taps and calculate the sum
try
{
for (int j = 0; j < myTaps; j++)
y = y + (x[j] * myCoeff[j]);
}
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine(e.Message + " Check filter order.");
throw;
}
//Shift all x values by 1 to the right
for (int j = myTaps - 1; j > 0; j--)
x[j] = x[j - 1];
//Add the y value to the output series at the current x value
myFilterSeries.Points.Add(new System.Web.UI.DataVisualization.Charting.DataPoint(i, y));
}
}
#endregion
}
}

View File

@@ -0,0 +1,372 @@
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
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,187 @@
//=================================================================
// File: SpikeRemoval.cs
//
// Namespace: System.Web.UI.DataVisualization.Charting.Utilities
//
// Classes: SpikeRemoval
//
// Purpose: Removes spikes from data
//
//
//===================================================================
// Chart Control for ASP.Net
// Copyright ?Microsoft Corporation, all rights reserved
//===================================================================
using System;
using System.Data;
using System.Configuration;
using System.Drawing;
using System.Web.UI.DataVisualization.Charting;
namespace System.Web.UI.DataVisualization.Charting.Utilities
{
/// <summary>
/// Spike removal is a utility used to remove high and low spikes from a graph. This means that the
/// chart axis scaling will be changed so that data that was difficult to see and analyze will be easier
/// to see after anomaly spikes have been removed.
/// </summary>
public class SpikeRemoval
{
#region Members
private bool mySetCutoffLabels;
private MarkerStyle myRemovedPointStyle;
private float myMaximum;
private float myMinimum;
#endregion
#region Properties
/// <summary>
/// Sets whether or not labels are set on each cut off point. If they are, they will show up on the chart and provide
/// extra clarification if the tooltip is not enough.
/// </summary>
public bool SetCutoffLabels
{
get { return mySetCutoffLabels; }
set { mySetCutoffLabels = value; }
}
/// <summary>
/// Holds the style that is used for the marker of any deleted points.
/// </summary>
public MarkerStyle RemovedPointStyle
{
get { return myRemovedPointStyle; }
set { myRemovedPointStyle = value; }
}
/// <summary>
/// Contains the maximum value of the data after it has had the spikes removed. This value also has
/// the tolerance factored in.
/// </summary>
public float Maximum
{
get { return myMaximum; }
}
/// <summary>
/// Contains the minimum value of the data after it has had the spikes removed. This value also has
/// the tolerance factored in.
/// </summary>
public float Minimum
{
get { return myMinimum; }
}
#endregion
#region Constructors
/// <summary>
/// Default Constructor.
/// </summary>
public SpikeRemoval()
{
//Default removed point style to a diamond.
myRemovedPointStyle = MarkerStyle.Diamond;
//Default labels to off.
mySetCutoffLabels = false;
}
#endregion
#region Public Methods
/// <summary>
/// RemoveSpikes will remove the high and low spikes off of a graph. The data within the series
/// provided will be modified, and for best results, the chart containing it should have axis
/// scaled automatically.
/// </summary>
/// <param name="dataseries">The series which contains the data to be analyzed and modified. It is assumed that
/// Y-values are contained in YValues[0]. Cases contrary to this will produce unexpected results.</param>
/// <param name="range">The percentage range of data to be kept. Anything that lies outside of the range
/// will be considered a spike.</param>
/// <param name="tolerance">The percentage a spike can be outside of the range but still included. The percentage
/// is based on the maximum or minimum value in the range.</param>
public void RemoveSpikes(Series dataseries, int range, int tolerance)
{
//Assure range and tolerance are a percentage.
//Range is more strict in that at least 1% of the data must be included in the range, whereas it is
//possible to have 0% tolerance.
if ((range < 1 || range > 100))
throw new ArgumentOutOfRangeException("range", "Range must be a percentage between 1 and 100");
if ((tolerance < 0 || tolerance > 100))
throw new ArgumentOutOfRangeException("tolerance", "Tolerance must be a percentage between 0 and 100");
//Data values and indices hold the y values and the indices of the points in arrays.
float[] datavalues = new float[dataseries.Points.Count];
int[] indices = new int[dataseries.Points.Count];
//Percent and number hold the actual values calculated from the range.
float percent = 0.0f;
int number = 0;
//Copy all y values into an array and store the indices.
for (int i = 0; i < dataseries.Points.Count; i++)
{
datavalues[i] = (float)dataseries.Points[i].YValues[0];
indices[i] = i;
}
//Sort the array and indices.
Array.Sort(datavalues, indices);
//Calculate the percent that has to come off each side of the data.
//ie. With a range of 80%, 20% of the data is being cut off, and 10% is coming off each side.
percent = ((100 - (float)range) / 2) / 100;
//Calculate the actual number of points coming off of each side.
//ie. With a percent of 10% and 100 data points, 10 points are coming off of each side.
number = (int)System.Math.Round(dataseries.Points.Count * percent, 0);
//Set the maximum and minimum values.
myMinimum = (float)(dataseries.Points[indices[number]].YValues[0] - System.Math.Abs(dataseries.Points[indices[number]].YValues[0] * (((float)tolerance / 100))));
myMaximum = (float)(dataseries.Points[indices[dataseries.Points.Count - number - 1]].YValues[0] + System.Math.Abs(dataseries.Points[indices[dataseries.Points.Count - number - 1]].YValues[0] * (((float)tolerance / 100))));
//Cut the low spikes off.
for (int i = 0; i < number; i++)
{
//Don't cut the spike if it's within the tolerance value.
if (dataseries.Points[indices[i]].YValues[0] < (myMinimum))
{
//Assign the tooltip to the point
dataseries.Points[indices[i]].ToolTip = "Value: " + dataseries.Points[indices[i]].YValues[0];
//Assign the label to the point
if (mySetCutoffLabels)
dataseries.Points[indices[i]].Label = "Value: " + dataseries.Points[indices[i]].YValues[0];
//Reassign the value to the minimum number allowed
dataseries.Points[indices[i]].YValues[0] = myMinimum;
//Set the marker point
dataseries.Points[indices[i]].MarkerStyle = myRemovedPointStyle;
}
}
//Cut the high spikes off.
for (int i = dataseries.Points.Count - number; i < dataseries.Points.Count; i++)
{
//Don't cut the spike if it's within the tolerance value.
if (dataseries.Points[indices[i]].YValues[0] > (myMaximum))
{
//Assign the tooltip to the point
dataseries.Points[indices[i]].ToolTip = "Value: " + dataseries.Points[indices[i]].YValues[0];
//Assign the label to the point
if (mySetCutoffLabels)
dataseries.Points[indices[i]].Label = "Value: " + dataseries.Points[indices[i]].YValues[0];
//Reassign the value to the maximum number allowed
dataseries.Points[indices[i]].YValues[0] = myMaximum;
//Set the marker point
dataseries.Points[indices[i]].MarkerStyle = myRemovedPointStyle;
}
}
}
#endregion
}
}

Binary file not shown.