namespace WinFormsChartSamples
{
using System;
using System.Drawing;
using System.Windows.Forms.DataVisualization.Charting;
/// field. Actual segment interval number can be slightly different due
/// to the automatic interval rounding.
///
public class HistogramChartHelper
{
#region Fields
///
/// Number of class intervals the data range is devided in.
/// This property only has affect when "SegmentIntervalWidth" is
/// set to double.NaN.
///
public int SegmentIntervalNumber = 20;
///
/// 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".
///
public double SegmentIntervalWidth = double.NaN;
///
/// Indicates that percent frequency should be shown on the right axis
///
public bool ShowPercentOnSecondaryYAxis = true;
#endregion // Fields
#region Methods
///
/// Creates a histogram chart.
///
/// Chart control reference.
/// Name of the series which stores the original data.
/// Name of the histogram series.
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);
}
// Adjust series attributes
histogramSeries["PointWidth"] = "1";
// Adjust chart area
ChartArea chartArea = chartControl.ChartAreas[histogramSeries.ChartArea];
chartArea.AxisY.Title = "Frequency";
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;
}
}
///
/// Helper method which rounds specified axsi interval.
///
/// Calculated axis interval.
/// Rounded axis interval.
internal 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
}
}