//------------------------------------------------------------- // // Copyright © Microsoft Corporation. All Rights Reserved. // //------------------------------------------------------------- // @owner=alexgor, deliant //================================================================= // File: ChartAreaAxes.cs // // Namespace: System.Web.UI.WebControls[Windows.Forms].Charting // // Classes: ChartAreaAxes // // Purpose: ChartAreaAxes is base class of Chart Area class. // This class searches for all series, which belongs // to this chart area and sets axes minimum and // maximum values using data. This class also checks // for chart types, which belong to this chart area // and prepare axis scale according to them (Stacked // chart types have different max and min values). // This class recognizes indexed values and prepares // axes for them. // // Reviewed: GS - Jul 31, 2002 // AG - August 7, 2002 // //=================================================================== #region Used namespaces using System; using System.Collections; using System.Collections.Generic; #if Microsoft_CONTROL using System.Windows.Forms.DataVisualization.Charting; using System.Windows.Forms.DataVisualization.Charting.Data; using System.Windows.Forms.DataVisualization.Charting.ChartTypes; using System.Windows.Forms.DataVisualization.Charting.Utilities; using System.Windows.Forms.DataVisualization.Charting.Borders3D; #else using System.Web.UI.DataVisualization.Charting.Data; using System.Web.UI.DataVisualization.Charting.ChartTypes; #endif #endregion #if Microsoft_CONTROL namespace System.Windows.Forms.DataVisualization.Charting #else namespace System.Web.UI.DataVisualization.Charting #endif { /// /// ChartAreaAxes class represents axes (X, Y, X2 and Y2) in the chart area. /// It contains methods that collect statistical information on the series data and /// other axes related methods. /// public partial class ChartArea { #region Fields // Axes which belong to this Chart Area internal Axis axisY = null; internal Axis axisX = null; internal Axis axisX2 = null; internal Axis axisY2 = null; // Array of series which belong to this chart area private List _series = new List(); // Array of chart types which belong to this chart area internal ArrayList chartTypes = new ArrayList(); /// /// List of series names that last interval numbers where cashed for /// private string _intervalSeriesList = ""; // Minimum interval between two data points for all // series which belong to this chart area. internal double intervalData = double.NaN; // Minimum interval between two data points for all // series which belong to this chart area. // IsLogarithmic version of the interval. internal double intervalLogData = double.NaN; // Series with minimum interval between two data points for all // series which belong to this chart area. private Series _intervalSeries = null; // Indicates that points are located through equal X intervals internal bool intervalSameSize = false; // Indicates that points alignment checked internal bool diffIntervalAlignmentChecked = false; // Chart Area contains stacked chart types internal bool stacked = false; // Chart type with two y values used for scale ( bubble chart type ) internal bool secondYScale = false; // The X and Y axes are switched internal bool switchValueAxes = false; // True for all chart types, which have axes. False for doughnut and pie chart. internal bool requireAxes = true; // Indicates that chart area has circular shape (like in radar or polar chart) internal bool chartAreaIsCurcular = false; // Chart Area contains 100 % stacked chart types internal bool hundredPercent = false; // Chart Area contains 100 % stacked chart types internal bool hundredPercentNegative = false; #endregion #region Internal properties /// /// True if sub axis supported on this chart area /// internal bool IsSubAxesSupported { get { if(((ChartArea)this).Area3DStyle.Enable3D || ((ChartArea)this).chartAreaIsCurcular) { return false; } return true; } } /// /// Data series which belongs to this chart area. /// internal List Series { get { return _series; } } /// /// Chart types which belongs to this chart area. /// internal ArrayList ChartTypes { get { return chartTypes; } } #endregion #region Methods /// /// Gets main or sub axis from the chart area. /// /// Axis name. NOTE: This parameter only defines X or Y axis. /// Second axisType parameter is used to select primary or secondary axis. /// Axis type. /// Sub-axis name or empty string. /// Main or sub axis of the chart area. internal Axis GetAxis(AxisName axisName, AxisType axisType, string subAxisName) { // Ignore sub axis in 3D if( ((ChartArea)this).Area3DStyle.Enable3D) { subAxisName = string.Empty; } if(axisName == AxisName.X || axisName == AxisName.X2) { if(axisType == AxisType.Primary) { return ((ChartArea)this).AxisX.GetSubAxis(subAxisName); } return ((ChartArea)this).AxisX2.GetSubAxis(subAxisName); } else { if(axisType == AxisType.Primary) { return ((ChartArea)this).AxisY.GetSubAxis(subAxisName); } return ((ChartArea)this).AxisY2.GetSubAxis(subAxisName); } } /// /// Sets default axis values for all different chart type /// groups. Chart type groups are sets of chart types. /// internal void SetDefaultAxesValues( ) { // The X and Y axes are switched ( Bar chart, stacked bar ... ) if( switchValueAxes ) { // Set axis positions axisY.AxisPosition = AxisPosition.Bottom; axisX.AxisPosition = AxisPosition.Left; axisX2.AxisPosition = AxisPosition.Right; axisY2.AxisPosition = AxisPosition.Top; } else { // Set axis positions axisY.AxisPosition = AxisPosition.Left; axisX.AxisPosition = AxisPosition.Bottom; axisX2.AxisPosition = AxisPosition.Top; axisY2.AxisPosition = AxisPosition.Right; } // Reset opposite Axes field. This cashing // value is used for optimization. foreach( Axis axisItem in ((ChartArea)this).Axes ) { axisItem.oppositeAxis = null; #if SUBAXES foreach( SubAxis subAxisItem in axisItem.SubAxes ) { subAxisItem.m_oppositeAxis = null; } #endif // SUBAXES } // *********************** // Primary X Axes // *********************** // Find the number of series which belong to this axis if (this.chartAreaIsCurcular) { // Set axis Maximum/Minimum and Interval for circular chart axisX.SetAutoMaximum(360.0); axisX.SetAutoMinimum(0.0); axisX.SetInterval = Math.Abs(axisX.maximum - axisX.minimum) / 12.0; } else { SetDefaultFromIndexesOrData(axisX, AxisType.Primary); } #if SUBAXES // *********************** // Primary X Sub-Axes // *********************** foreach(SubAxis subAxis in axisX.SubAxes) { SetDefaultFromIndexesOrData(subAxis, AxisType.Primary); } #endif // SUBAXES // *********************** // Secondary X Axes // *********************** SetDefaultFromIndexesOrData(axisX2, AxisType.Secondary); #if SUBAXES // *********************** // Secondary X Sub-Axes // *********************** foreach(SubAxis subAxis in axisX2.SubAxes) { SetDefaultFromIndexesOrData(subAxis, AxisType.Secondary); } #endif // SUBAXES // *********************** // Primary Y axis // *********************** if( GetYAxesSeries( AxisType.Primary, string.Empty ).Count != 0 ) { // Find minimum and maximum from Y values. SetDefaultFromData( axisY ); axisY.EstimateAxis(); } #if SUBAXES // *********************** // Primary Y Sub-Axes // *********************** foreach(SubAxis subAxis in axisY.SubAxes) { // Find the number of series which belong to this axis if( GetYAxesSeries( AxisType.Primary, subAxis.SubAxisName ).Count != 0 ) { // Find minimum and maximum from Y values. SetDefaultFromData( subAxis ); subAxis.EstimateAxis(); } } #endif // SUBAXES // *********************** // Secondary Y axis // *********************** if( GetYAxesSeries( AxisType.Secondary, string.Empty ).Count != 0 ) { // Find minimum and maximum from Y values. SetDefaultFromData( axisY2 ); axisY2.EstimateAxis(); } #if SUBAXES // *********************** // Secondary Y Sub-Axes // *********************** foreach(SubAxis subAxis in axisY2.SubAxes) { // Find the number of series which belong to this axis if( GetYAxesSeries( AxisType.Secondary, subAxis.SubAxisName ).Count != 0 ) { // Find minimum and maximum from Y values. SetDefaultFromData( subAxis ); subAxis.EstimateAxis(); } } #endif // SUBAXES // Sets axis position. Axis position depends // on crossing and reversed value. axisX.SetAxisPosition(); axisX2.SetAxisPosition(); axisY.SetAxisPosition(); axisY2.SetAxisPosition(); // Enable axes, which are // used in data series. this.EnableAxes(); // Get scale break segments Axis[] axesYArray = new Axis[] { axisY, axisY2 }; foreach(Axis currentAxis in axesYArray) { // Get automatic scale break segments currentAxis.ScaleBreakStyle.GetAxisSegmentForScaleBreaks(currentAxis.ScaleSegments); // Make sure axis scale do not exceed segments scale if(currentAxis.ScaleSegments.Count > 0) { // Save flag that scale segments are used currentAxis.scaleSegmentsUsed = true; if(currentAxis.minimum < currentAxis.ScaleSegments[0].ScaleMinimum) { currentAxis.minimum = currentAxis.ScaleSegments[0].ScaleMinimum; } if(currentAxis.minimum > currentAxis.ScaleSegments[currentAxis.ScaleSegments.Count - 1].ScaleMaximum) { currentAxis.minimum = currentAxis.ScaleSegments[currentAxis.ScaleSegments.Count - 1].ScaleMaximum; } } } bool useScaleSegments = false; // Fill Labels Axis[] axesArray = new Axis[] { axisX, axisX2, axisY, axisY2 }; foreach(Axis currentAxis in axesArray) { useScaleSegments = (currentAxis.ScaleSegments.Count > 0); if(!useScaleSegments) { currentAxis.FillLabels(true); } else { bool removeLabels = true; int segmentIndex = 0; foreach(AxisScaleSegment scaleSegment in currentAxis.ScaleSegments) { scaleSegment.SetTempAxisScaleAndInterval(); currentAxis.FillLabels(removeLabels); removeLabels = false; scaleSegment.RestoreAxisScaleAndInterval(); // Remove last label for all segments except of the last if(segmentIndex < (currentAxis.ScaleSegments.Count - 1) && currentAxis.CustomLabels.Count > 0) { currentAxis.CustomLabels.RemoveAt(currentAxis.CustomLabels.Count - 1); } ++segmentIndex; } } } foreach (Axis currentAxis in axesArray) { currentAxis.PostFillLabels(); } } /// /// Sets the axis defaults. /// If the at least one of the series bound to this axis is Indexed then the defaults are set using the SetDefaultsFromIndexes(). /// Otherwise the SetDefaultFromData() is used. /// /// Axis to process /// Axis type private void SetDefaultFromIndexesOrData(Axis axis, AxisType axisType) { //Get array of the series that are linked to this axis List axisSeriesNames = GetXAxesSeries(axisType, axis.SubAxisName); // VSTS: 196381 // before this change: If we find one indexed series we will treat all series as indexed. // after this change : We will assume that all series are indexed. // If we find one non indexed series we will treat all series as non indexed. bool indexedSeries = true; // DT comments 1: // If we have mix of indexed with non-indexed series // enforce all indexed series as non-indexed; // The result of mixed type of series will be more natural // and easy to detect the problem - all datapoints of indexed // series will be displayed on zero position. //===================================== // bool nonIndexedSeries = false; //======================================= //Loop through the series looking for a indexed one foreach(string seriesName in axisSeriesNames) { // Get series Series series = Common.DataManager.Series[seriesName]; // Check if series is indexed if (!ChartHelper.IndexedSeries(series)) { // found one nonindexed series - we will treat all series as non indexed. indexedSeries = false; break; } // DT comments 2 //else //{ // nonIndexedSeries = true; //} } //DT comments 3 //if (!indexedSeries && nonIndexedSeries) //{ // foreach (string seriesName in axisSeriesNames) // { // // Get series // Series series = Common.DataManager.Series[seriesName]; // series.xValuesZeros = false; // } //} if (indexedSeries) { if (axis.IsLogarithmic) { throw (new InvalidOperationException(SR.ExceptionChartAreaAxisScaleLogarithmicUnsuitable)); } //Set axis defaults from the indexed series SetDefaultFromIndexes(axis); //We are done... return; } // If haven't found any indexed series -> Set axis defaults from the series data SetDefaultFromData(axis); axis.EstimateAxis(); } /// /// Enable axes, which are /// used in chart area data series. /// private void EnableAxes() { if( _series == null ) { return; } bool activeX = false; bool activeY = false; bool activeX2 = false; bool activeY2 = false; // Data series from this chart area foreach( string ser in _series ) { Series dataSeries = Common.DataManager.Series[ ser ]; // X axes if( dataSeries.XAxisType == AxisType.Primary ) { activeX = true; #if SUBAXES this.Activate( axisX, true, dataSeries.XSubAxisName ); #else this.Activate( axisX, true ); #endif // SUBAXES } else { activeX2 = true; #if SUBAXES this.Activate( axisX2, true, dataSeries.XSubAxisName ); #else this.Activate( axisX2, true ); #endif // SUBAXES } // Y axes if( dataSeries.YAxisType == AxisType.Primary ) { activeY = true; #if SUBAXES this.Activate( axisY, true, dataSeries.YSubAxisName ); #else this.Activate( axisY, true ); #endif // SUBAXES } else { activeY2 = true; #if SUBAXES this.Activate( axisY2, true, dataSeries.YSubAxisName ); #else this.Activate( axisY2, true ); #endif // SUBAXES } } #if SUBAXES // Enable Axes if(!activeX) this.Activate( axisX, false, string.Empty ); if(!activeY) this.Activate( axisY, false, string.Empty ); if(!activeX2) this.Activate( axisX2, false, string.Empty ); if(!activeY2) this.Activate( axisY2, false, string.Empty ); #else // SUBAXES // Enable Axes if(!activeX) this.Activate( axisX, false); if(!activeY) this.Activate( axisY, false); if(!activeX2) this.Activate( axisX2, false); if(!activeY2) this.Activate( axisY2, false); #endif // SUBAXES } #if SUBAXES /// /// Enable axis. /// /// Axis. /// True if axis is active. /// Sub axis name to activate. private void Activate( Axis axis, bool active, string subAxisName ) { // Auto-Enable axis if( axis.autoEnabled == true ) { axis.enabled = active; } // Auto-Enable sub axes if(subAxisName.Length > 0) { SubAxis subAxis = axis.SubAxes.FindByName(subAxisName); if(subAxis != null) { if( subAxis.autoEnabled == true ) { subAxis.enabled = active; } } } } #else /// /// Enable axis. /// /// Axis. /// True if axis is active. private void Activate( Axis axis, bool active ) { if( axis.autoEnabled == true ) { axis.enabled = active; } } #endif // SUBAXES /// /// Check if all data points from series in /// this chart area are empty. /// /// True if all points are empty bool AllEmptyPoints() { // Data series from this chart area foreach( string seriesName in this._series ) { Series dataSeries = Common.DataManager.Series[ seriesName ]; // Data point loop foreach( DataPoint point in dataSeries.Points ) { if( !point.IsEmpty ) { return false; } } } return true; } /// /// This method sets default minimum and maximum /// values from values in the data manager. This /// case is used if X values are not equal to 0 or IsXValueIndexed flag is set. /// /// Axis private void SetDefaultFromData( Axis axis ) { #if SUBAXES // Process all sub-axes if(!axis.IsSubAxis) { foreach(SubAxis subAxis in axis.SubAxes) { this.SetDefaultFromData( subAxis ); } } #endif // SUBAXES // Used for scrolling with logarithmic axes. if( !Double.IsNaN(axis.ScaleView.Position) && !Double.IsNaN(axis.ScaleView.Size) && !axis.refreshMinMaxFromData && axis.IsLogarithmic ) { return; } // Get minimum and maximum from data source double autoMaximum; double autoMinimum; this.GetValuesFromData( axis, out autoMinimum, out autoMaximum ); // *************************************************** // This part of code is used to add a margin to the // axis and to set minimum value to zero if // IsStartedFromZero property is used. There is special // code for logarithmic scale, which will set minimum // to one instead of zero. // *************************************************** // The minimum and maximum values from data manager don’t exist. if( axis.enabled && ( (axis.AutoMaximum || double.IsNaN( axis.Maximum )) && (autoMaximum == Double.MaxValue || autoMaximum == Double.MinValue)) || ( (axis.AutoMinimum || double.IsNaN( axis.Minimum )) && (autoMinimum == Double.MaxValue || autoMinimum == Double.MinValue )) ) { if( this.AllEmptyPoints() ) { // Supress exception and use predefined min & max autoMaximum = 8.0; autoMinimum = 1.0; } else { if(!this.Common.ChartPicture.SuppressExceptions) { throw (new InvalidOperationException(SR.ExceptionAxisMinimumMaximumInvalid)); } } } // Axis margin used for zooming axis.marginView = 0.0; if( axis.margin == 100 && (axis.axisType == AxisName.X || axis.axisType == AxisName.X2) ) { axis.marginView = this.GetPointsInterval( false, 10 ); } // If minimum and maximum are same margin always exist. if( autoMaximum == autoMinimum && axis.Maximum == axis.Minimum ) { axis.marginView = 1; } // Do not make axis margine for logarithmic axes if( axis.IsLogarithmic ) { axis.marginView = 0.0; } // Adjust Maximum - Add a gap if( axis.AutoMaximum ) { // Add a Gap for X axis if( !axis.roundedXValues && ( axis.axisType == AxisName.X || axis.axisType == AxisName.X2 ) ) { axis.SetAutoMaximum( autoMaximum + axis.marginView ); } else { if( axis.isStartedFromZero && autoMaximum < 0 ) { axis.SetAutoMaximum( 0.0 ); } else { axis.SetAutoMaximum( autoMaximum ); } } } // Adjust Minimum - make rounded values and add a gap if( axis.AutoMinimum ) { // IsLogarithmic axis if( axis.IsLogarithmic ) { if( autoMinimum < 1.0 ) { axis.SetAutoMinimum( autoMinimum ); } else if( axis.isStartedFromZero ) { axis.SetAutoMinimum( 1.0 ); } else { axis.SetAutoMinimum( autoMinimum ); } } else { if( autoMinimum > 0.0 ) // If Auto calculated Minimum value is positive { // Adjust Minimum if( !axis.roundedXValues && ( axis.axisType == AxisName.X || axis.axisType == AxisName.X2 ) ) { axis.SetAutoMinimum( autoMinimum - axis.marginView ); } // If start From Zero property is true 0 is always on the axis. // NOTE: Not applicable if date-time values are drawn. Fixes issue #5644 else if( axis.isStartedFromZero && !this.SeriesDateTimeType( axis.axisType, axis.SubAxisName ) ) { axis.SetAutoMinimum( 0.0 ); } else { axis.SetAutoMinimum( autoMinimum ); } } else // If Auto calculated Minimum value is non positive { if( axis.axisType == AxisName.X || axis.axisType == AxisName.X2 ) { axis.SetAutoMinimum( autoMinimum - axis.marginView ); } else { // If start From Zero property is true 0 is always on the axis. axis.SetAutoMinimum( autoMinimum ); } } } } // If maximum or minimum are not auto set value to non logarithmic if( axis.IsLogarithmic && axis.logarithmicConvertedToLinear ) { if( !axis.AutoMinimum ) { axis.minimum = axis.logarithmicMinimum; } if( !axis.AutoMaximum ) { axis.maximum = axis.logarithmicMaximum; } // Min and max will take real values again if scale is logarithmic. axis.logarithmicConvertedToLinear = false; } // Check if Minimum == Maximum if(this.Common.ChartPicture.SuppressExceptions && axis.maximum == axis.minimum) { axis.minimum = axis.maximum; axis.maximum = axis.minimum + 1.0; } } /// /// This method checks if all series in the chart area have “integer type” /// for specified axes, which means int, uint, long and ulong. /// /// Name of the axis /// Sub axis name. /// True if all series are integer [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "subAxisName")] internal bool SeriesIntegerType( AxisName axisName, string subAxisName ) { // Series which belong to this chart area foreach( string seriesName in this._series ) { Series ser = Common.DataManager.Series[ seriesName ]; // X axes type if( axisName == AxisName.X ) { #if SUBAXES if( ser.XAxisType == AxisType.Primary && ser.XSubAxisName == subAxisName) #else //SUBAXES if ( ser.XAxisType == AxisType.Primary) #endif //SUBAXES { if(ser.XValueType != ChartValueType.Int32 && ser.XValueType != ChartValueType.UInt32 && ser.XValueType != ChartValueType.UInt64 && ser.XValueType != ChartValueType.Int64 ) { return false; } else { return true; } } } // X axes type else if( axisName == AxisName.X2 ) { #if SUBAXES if( ser.XAxisType == AxisType.Secondary && ser.XSubAxisName == subAxisName) #else //SUBAXES if ( ser.XAxisType == AxisType.Secondary) #endif //SUBAXES { if(ser.XValueType != ChartValueType.Int32 && ser.XValueType != ChartValueType.UInt32 && ser.XValueType != ChartValueType.UInt64 && ser.XValueType != ChartValueType.Int64 ) { return false; } else { return true; } } } // Y axes type else if( axisName == AxisName.Y ) { #if SUBAXES if( ser.YAxisType == AxisType.Primary && ser.YSubAxisName == subAxisName) #else //SUBAXES if ( ser.YAxisType == AxisType.Primary) #endif //SUBAXES { if(ser.YValueType != ChartValueType.Int32 && ser.YValueType != ChartValueType.UInt32 && ser.YValueType != ChartValueType.UInt64 && ser.YValueType != ChartValueType.Int64 ) { return false; } else { return true; } } } else if( axisName == AxisName.Y2 ) { #if SUBAXES if( ser.YAxisType == AxisType.Secondary && ser.YSubAxisName == subAxisName) #else //SUBAXES if ( ser.YAxisType == AxisType.Secondary) #endif //SUBAXES { if(ser.YValueType != ChartValueType.Int32 && ser.YValueType != ChartValueType.UInt32 && ser.YValueType != ChartValueType.UInt64 && ser.YValueType != ChartValueType.Int64 ) { return false; } else { return true; } } } } return false; } /// /// This method checks if all series in the chart area have “date-time type” /// for specified axes. /// /// Name of the axis /// Sub axis name. /// True if all series are date-time. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "subAxisName")] internal bool SeriesDateTimeType( AxisName axisName, string subAxisName ) { // Series which belong to this chart area foreach( string seriesName in this._series ) { Series ser = Common.DataManager.Series[ seriesName ]; // X axes type if( axisName == AxisName.X ) { #if SUBAXES if( ser.XAxisType == AxisType.Primary && ser.XSubAxisName == subAxisName) #else //SUBAXES if ( ser.XAxisType == AxisType.Primary) #endif //SUBAXES { if(ser.XValueType != ChartValueType.Date && ser.XValueType != ChartValueType.DateTime && ser.XValueType != ChartValueType.Time && ser.XValueType != ChartValueType.DateTimeOffset) { return false; } else { return true; } } } // X axes type else if( axisName == AxisName.X2 ) { #if SUBAXES if( ser.XAxisType == AxisType.Secondary && ser.XSubAxisName == subAxisName) #else //SUBAXES if ( ser.XAxisType == AxisType.Secondary) #endif //SUBAXES { if(ser.XValueType != ChartValueType.Date && ser.XValueType != ChartValueType.DateTime && ser.XValueType != ChartValueType.Time && ser.XValueType != ChartValueType.DateTimeOffset) { return false; } else { return true; } } } // Y axes type else if( axisName == AxisName.Y ) { #if SUBAXES if( ser.YAxisType == AxisType.Primary && ser.YSubAxisName == subAxisName) #else //SUBAXES if ( ser.YAxisType == AxisType.Primary) #endif //SUBAXES { if(ser.YValueType != ChartValueType.Date && ser.YValueType != ChartValueType.DateTime && ser.YValueType != ChartValueType.Time && ser.YValueType != ChartValueType.DateTimeOffset) { return false; } else { return true; } } } else if( axisName == AxisName.Y2 ) { #if SUBAXES if( ser.YAxisType == AxisType.Secondary && ser.YSubAxisName == subAxisName) #else //SUBAXES if ( ser.YAxisType == AxisType.Secondary) #endif //SUBAXES { if(ser.YValueType != ChartValueType.Date && ser.YValueType != ChartValueType.DateTime && ser.YValueType != ChartValueType.Time && ser.YValueType != ChartValueType.DateTimeOffset) { return false; } else { return true; } } } } return false; } /// /// This method calculates minimum and maximum from data series. /// /// Axis which is used to find minimum and maximum /// Minimum value from data. /// Maximum value from data. private void GetValuesFromData( Axis axis, out double autoMinimum, out double autoMaximum ) { // Get number of points in series int currentPointsNumber = this.GetNumberOfAllPoints(); if( !axis.refreshMinMaxFromData && !double.IsNaN(axis.minimumFromData) && !double.IsNaN(axis.maximumFromData) && axis.numberOfPointsInAllSeries == currentPointsNumber ) { autoMinimum = axis.minimumFromData; autoMaximum = axis.maximumFromData; return; } // Set Axis type AxisType type = AxisType.Primary; if( axis.axisType == AxisName.X2 || axis.axisType == AxisName.Y2 ) { type = AxisType.Secondary; } // Creates a list of series, which have same X axis type. string [] xAxesSeries = GetXAxesSeries(type, axis.SubAxisName).ToArray(); // Creates a list of series, which have same Y axis type. string [] yAxesSeries = GetYAxesSeries( type, axis.SubAxisName ).ToArray(); // Get auto maximum and auto minimum value if( axis.axisType == AxisName.X2 || axis.axisType == AxisName.X ) // X axis type is used (X or X2) { if( stacked ) // Chart area has a stacked chart types { try { Common.DataManager.GetMinMaxXValue(out autoMinimum, out autoMaximum, xAxesSeries ); } catch(System.Exception) { throw (new InvalidOperationException(SR.ExceptionAxisStackedChartsDataPointsNumberMismatch)); } } // Chart type with two y values used for scale ( bubble chart type ) else if( secondYScale ) { autoMaximum = Common.DataManager.GetMaxXWithRadiusValue( (ChartArea)this, xAxesSeries ); autoMinimum = Common.DataManager.GetMinXWithRadiusValue( (ChartArea)this, xAxesSeries ); ChartValueType valueTypes = Common.DataManager.Series[xAxesSeries[0]].XValueType; if( valueTypes != ChartValueType.Date && valueTypes != ChartValueType.DateTime && valueTypes != ChartValueType.Time && valueTypes != ChartValueType.DateTimeOffset ) { axis.roundedXValues = true; } } else { Common.DataManager.GetMinMaxXValue(out autoMinimum, out autoMaximum, xAxesSeries ); } } else // Y axis type is used (Y or Y2) { // ***************************** // Stacked Chart AxisName // ***************************** if( stacked ) // Chart area has a stacked chart types { try { if(hundredPercent) // It's a hundred percent stacked chart { autoMaximum = Common.DataManager.GetMaxHundredPercentStackedYValue(hundredPercentNegative, yAxesSeries ); autoMinimum = Common.DataManager.GetMinHundredPercentStackedYValue(hundredPercentNegative, yAxesSeries ); } else { // If stacked groupes are used Min/Max range must calculated // for each group seperatly. double stackMaxBarColumn = double.MinValue; double stackMinBarColumn = double.MaxValue; double stackMaxArea = double.MinValue; double stackMinArea = double.MaxValue; // Split series by group names ArrayList stackedGroups = this.SplitSeriesInStackedGroups(yAxesSeries); foreach(string[] groupSeriesNames in stackedGroups) { // For stacked bar and column double stackMaxBarColumnForGroup = Common.DataManager.GetMaxStackedYValue(0, groupSeriesNames ); double stackMinBarColumnForGroup = Common.DataManager.GetMinStackedYValue(0, groupSeriesNames ); // For stacked area double stackMaxAreaForGroup = Common.DataManager.GetMaxUnsignedStackedYValue(0, groupSeriesNames ); double stackMinAreaForGroup = Common.DataManager.GetMinUnsignedStackedYValue(0, groupSeriesNames ); // Select minimum/maximum stackMaxBarColumn = Math.Max(stackMaxBarColumn, stackMaxBarColumnForGroup); stackMinBarColumn = Math.Min(stackMinBarColumn, stackMinBarColumnForGroup); stackMaxArea = Math.Max(stackMaxArea, stackMaxAreaForGroup); stackMinArea = Math.Min(stackMinArea, stackMinAreaForGroup); } autoMaximum = Math.Max(stackMaxBarColumn,stackMaxArea); autoMinimum = Math.Min(stackMinBarColumn,stackMinArea); } // IsLogarithmic axis if( axis.IsLogarithmic && autoMinimum < 1.0 ) autoMinimum = 1.0; } catch(System.Exception) { throw (new InvalidOperationException(SR.ExceptionAxisStackedChartsDataPointsNumberMismatch)); } } // Chart type with two y values used for scale ( bubble chart type ) else if( secondYScale ) { autoMaximum = Common.DataManager.GetMaxYWithRadiusValue( (ChartArea)this, yAxesSeries ); autoMinimum = Common.DataManager.GetMinYWithRadiusValue( (ChartArea)this, yAxesSeries ); } // ***************************** // Non Stacked Chart Types // ***************************** else { // Check if any series in the area has ExtraYValuesConnectedToYAxis flag set bool extraYValuesConnectedToYAxis = false; if(this.Common != null && this.Common.Chart != null) { foreach(Series series in this.Common.Chart.Series) { if(series.ChartArea == ((ChartArea)this).Name) { IChartType charType = Common.ChartTypeRegistry.GetChartType( series.ChartTypeName ); if(charType != null && charType.ExtraYValuesConnectedToYAxis) { extraYValuesConnectedToYAxis = true; break; } } } } // The first Chart type can have many Y values (Stock Chart, Range Chart) if( extraYValuesConnectedToYAxis ) { Common.DataManager.GetMinMaxYValue(out autoMinimum, out autoMaximum, yAxesSeries ); } else { // The first Chart type can have only one Y value Common.DataManager.GetMinMaxYValue(0, out autoMinimum, out autoMaximum, yAxesSeries ); } } } // Store Minimum and maximum from data. There is no // reason to calculate this values every time. axis.maximumFromData = autoMaximum; axis.minimumFromData = autoMinimum; axis.refreshMinMaxFromData = false; // Make extra test for stored minimum and maximum values // from data. If Number of points is different then data // source is changed. That means that we should read // data again. axis.numberOfPointsInAllSeries = currentPointsNumber; } /// /// Splits a single array of series names into multiple arrays /// based on the stacked group name. /// /// Array of series name to split. /// An array list that contains sub-arrays of series names split by group name. private ArrayList SplitSeriesInStackedGroups(string[] seriesNames) { Hashtable groupsHashTable = new Hashtable(); foreach(string seriesName in seriesNames) { // Get series object Series series = this.Common.Chart.Series[seriesName]; // NOTE: Fix for issue #6716 // Double check that series supports stacked group feature string groupName = string.Empty; if(StackedColumnChart.IsSeriesStackGroupNameSupported(series)) { // Get stacked group name (empty string by default) groupName = StackedColumnChart.GetSeriesStackGroupName(series); } // Check if this group was alreday added in to the hashtable if (groupsHashTable.ContainsKey(groupName)) { ArrayList list = (ArrayList)groupsHashTable[groupName]; list.Add(seriesName); } else { ArrayList list = new ArrayList(); list.Add(seriesName); groupsHashTable.Add(groupName, list); } } // Convert results to a list that contains array of strings ArrayList result = new ArrayList(); foreach(DictionaryEntry entry in groupsHashTable) { ArrayList list = (ArrayList)entry.Value; if(list.Count > 0) { int index = 0; string[] stringArray = new String[list.Count]; foreach(string str in list) { stringArray[index++] = str; } result.Add(stringArray); } } return result; } /// /// Find number of points for all series /// /// Number of points private int GetNumberOfAllPoints() { int numOfPoints = 0; foreach( Series series in Common.DataManager.Series ) { numOfPoints += series.Points.Count; } return numOfPoints; } /// /// This method sets default minimum and maximum values from /// indexes. This case is used if all X values in a series /// have 0 value or IsXValueIndexed flag is set. /// /// Axis private void SetDefaultFromIndexes( Axis axis ) { // Adjust margin for side-by-side charts like column axis.SetTempAxisOffset( ); // Set Axis type AxisType type = AxisType.Primary; if( axis.axisType == AxisName.X2 || axis.axisType == AxisName.Y2 ) { type = AxisType.Secondary; } // The maximum is equal to the number of data points. double autoMaximum = Common.DataManager.GetNumberOfPoints( GetXAxesSeries( type, axis.SubAxisName ).ToArray() ); double autoMinimum = 0.0; // Axis margin used only for zooming axis.marginView = 0.0; if( axis.margin == 100 ) axis.marginView = 1.0; // If minimum and maximum are same margin always exist. if( autoMaximum + axis.margin/100 == autoMinimum - axis.margin/100 + 1 ) { // Set Maximum Number. axis.SetAutoMaximum( autoMaximum + 1 ); axis.SetAutoMinimum( autoMinimum ); } else // Nomal case { // Set Maximum Number. axis.SetAutoMaximum( autoMaximum + axis.margin/100 ); axis.SetAutoMinimum( autoMinimum - axis.margin/100 + 1 ); } // Find the interval. If the nuber of points // is less then 10 interval is 1. double axisInterval; if( axis.ViewMaximum - axis.ViewMinimum <= 10 ) { axisInterval = 1.0; } else { axisInterval = axis.CalcInterval( ( axis.ViewMaximum - axis.ViewMinimum ) / 5 ); } ChartArea area = (ChartArea)this; if( area.Area3DStyle.Enable3D && !double.IsNaN(axis.interval3DCorrection) ) { axisInterval = Math.Ceiling( axisInterval / axis.interval3DCorrection ); axis.interval3DCorrection = double.NaN; // Use interval if( axisInterval > 1.0 && axisInterval < 4.0 && axis.ViewMaximum - axis.ViewMinimum <= 4 ) { axisInterval = 1.0; } } axis.SetInterval = axisInterval; // If temporary offsets were defined for the margin, // adjust offset for minor ticks and grids. if(axis.offsetTempSet) { axis.minorGrid.intervalOffset -= axis.MajorGrid.GetInterval(); axis.minorTickMark.intervalOffset -= axis.MajorTickMark.GetInterval(); } } /// /// Sets the names of all data series which belong to /// this chart area to collection and sets a list of all /// different chart types. /// internal void SetData() { this.SetData(true, true); } /// /// Sets the names of all data series which belong to /// this chart area to collection and sets a list of all /// different chart types. /// /// If set to true the method will initialize axes default values. /// If set to true the method will check that all primary X axis series are aligned if use the IsXValueIndexed flag. internal void SetData( bool initializeAxes, bool checkIndexedAligned) { // Initialize chart type properties stacked = false; switchValueAxes = false; requireAxes = true; hundredPercent = false; hundredPercentNegative = false; chartAreaIsCurcular = false; secondYScale = false; // AxisName of the chart area already set. bool typeSet = false; // Remove all elements from the collection this._series.Clear(); // Add series to the collection foreach( Series series in Common.DataManager.Series ) { if (series.ChartArea == this.Name && series.IsVisible() && series.Points.Count > 0) { this._series.Add(series.Name); } } // Remove all elements from the collection this.chartTypes.Clear(); // Add series to the collection foreach( Series series in Common.DataManager.Series ) { // A item already exist. bool foundItem = false; if (series.IsVisible() && series.ChartArea==this.Name) { foreach( string type in chartTypes ) { // AxisName already exist in the chart area if( type == series.ChartTypeName ) { foundItem = true; } } // Add chart type to the collection of // Chart area's chart types if( !foundItem ) { // Set stacked type if( Common.ChartTypeRegistry.GetChartType(series.ChartTypeName).Stacked ) { stacked = true; } if( !typeSet ) { if( Common.ChartTypeRegistry.GetChartType(series.ChartTypeName).SwitchValueAxes ) switchValueAxes = true; if( !Common.ChartTypeRegistry.GetChartType(series.ChartTypeName).RequireAxes ) requireAxes = false; if( Common.ChartTypeRegistry.GetChartType(series.ChartTypeName).CircularChartArea ) chartAreaIsCurcular = true; if( Common.ChartTypeRegistry.GetChartType(series.ChartTypeName).HundredPercent ) hundredPercent = true; if( Common.ChartTypeRegistry.GetChartType(series.ChartTypeName).HundredPercentSupportNegative ) hundredPercentNegative = true; if( Common.ChartTypeRegistry.GetChartType(series.ChartTypeName).SecondYScale ) secondYScale = true; typeSet = true; } else { if( Common.ChartTypeRegistry.GetChartType(series.ChartTypeName).SwitchValueAxes != switchValueAxes ) { throw (new InvalidOperationException(SR.ExceptionChartAreaChartTypesCanNotCombine)); } } // Series is not empty if( Common.DataManager.GetNumberOfPoints( series.Name ) != 0 ) { this.chartTypes.Add( series.ChartTypeName ); } } } } // Check that all primary X axis series are aligned if use the IsXValueIndexed flag if (checkIndexedAligned) { for (int axisIndex = 0; axisIndex <= 1; axisIndex++) { List seriesArray = this.GetXAxesSeries((axisIndex == 0) ? AxisType.Primary : AxisType.Secondary, string.Empty); if (seriesArray.Count > 0) { bool indexed = false; string seriesNamesStr = ""; foreach (string seriesName in seriesArray) { seriesNamesStr = seriesNamesStr + seriesName.Replace(",", "\\,") + ","; if (Common.DataManager.Series[seriesName].IsXValueIndexed) { indexed = true; } } if (indexed) { try { Common.DataManipulator.CheckXValuesAlignment( Common.DataManipulator.ConvertToSeriesArray(seriesNamesStr.TrimEnd(','), false)); } catch (Exception e) { throw (new ArgumentException(SR.ExceptionAxisSeriesNotAligned + e.Message)); } } } } } if (initializeAxes) { // Set default min, max etc. SetDefaultAxesValues(); } } /// /// Returns names of all series, which belong to this chart area /// and have same chart type. /// /// Chart type /// Collection with series names internal List GetSeriesFromChartType( string chartType ) { // New collection List list = new List(); foreach( string seriesName in _series ) { if( String.Compare( chartType, Common.DataManager.Series[seriesName].ChartTypeName, StringComparison.OrdinalIgnoreCase ) == 0 ) { // Add a series name to the collection list.Add( seriesName ); } } return list; } /// /// Returns all series which belong to this chart area. /// /// Collection with series internal List GetSeries( ) { // New collection List list = new List(); foreach( string seriesName in _series ) { list.Add(Common.DataManager.Series[seriesName]); } return list; } /// /// Creates a list of series, which have same X axis type. /// /// Axis type /// Sub Axis name /// A list of series internal List GetXAxesSeries( AxisType type, string subAxisName ) { // Create a new collection of series List list = new List(); if (_series.Count == 0) { return list; } // Ignore sub axis in 3D if( !this.IsSubAxesSupported ) { if(subAxisName.Length > 0) { return list; } } // Find series which have same axis type foreach( string ser in _series ) { #if SUBAXES if( Common.DataManager.Series[ser].XAxisType == type && (Common.DataManager.Series[ser].XSubAxisName == subAxisName || !this.IsSubAxesSupported) ) #else // SUBAXES if ( Common.DataManager.Series[ser].XAxisType == type) #endif // SUBAXES { // Add a series to the collection list.Add( ser ); } } #if SUBAXES // If series list is empty for the sub-axis then // try using the main axis. if ( list.Count == 0 && subAxisName.Length > 0 ) { return GetXAxesSeries( type, string.Empty ); } #endif // SUBAXES // If primary series do not exist return secondary series // Axis should always be connected with any series. if ( list.Count == 0 ) { if (type == AxisType.Secondary) { return GetXAxesSeries(AxisType.Primary, string.Empty); } return GetXAxesSeries(AxisType.Secondary, string.Empty); } return list; } /// /// Creates a list of series, which have same Y axis type. /// /// Axis type /// Sub Axis name /// A list of series [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "subAxisName")] internal List GetYAxesSeries( AxisType type, string subAxisName ) { // Create a new collection of series List list = new List(); // Find series which have same axis type foreach( string ser in _series ) { // Get series Y axis type AxisType seriesYAxisType = Common.DataManager.Series[ser].YAxisType; #if SUBAXES string seriesYSubAxisName = subAxisName; #endif // SUBAXES // NOTE: Fixes issue #6969 // Ignore series settings if only Primary Y axis supported by the chart type if (Common.DataManager.Series[ser].ChartType == SeriesChartType.Radar || Common.DataManager.Series[ser].ChartType == SeriesChartType.Polar) { seriesYAxisType = AxisType.Primary; #if SUBAXES seriesYSubAxisName = string.Empty; #endif // SUBAXES } #if SUBAXES if( seriesYAxisType == type && (Common.DataManager.Series[ser].YSubAxisName == seriesYSubAxisName || !this.IsSubAxesSupported) ) #else // SUBAXES if (seriesYAxisType == type) #endif // SUBAXES { // Add a series to the collection list.Add( ser ); } } #if SUBAXES // If series list is empty for the sub-axis then // try using the main axis. if ( list.Count == 0 && subAxisName.Length > 0 ) { return GetYAxesSeries( type, string.Empty ); } #endif // SUBAXES // If primary series do not exist return secondary series // Axis should always be connected with any series. if ( list.Count == 0 && type == AxisType.Secondary ) { return GetYAxesSeries( AxisType.Primary, string.Empty ); } return list; } /// /// Get first series from the chart area /// /// Data series internal Series GetFirstSeries() { if( _series.Count == 0 ) { throw (new InvalidOperationException(SR.ExceptionChartAreaSeriesNotFound)); } return Common.DataManager.Series[_series[0]]; } /// /// This method returns minimum interval between /// any two data points from series which belong /// to this chart area. /// /// Indicates logarithmic scale. /// Logarithm Base /// Minimum Interval internal double GetPointsInterval(bool isLogarithmic, double logarithmBase) { bool sameInterval; return GetPointsInterval( _series, isLogarithmic, logarithmBase, false, out sameInterval ); } /// /// This method returns minimum interval between /// any two data points from specified series. /// /// List of series. /// Indicates logarithmic scale. /// Base for logarithmic base /// True if check for the same interval should be performed. /// Return true if interval is the same. /// Minimum Interval internal double GetPointsInterval( List seriesList, bool isLogarithmic, double logarithmBase, bool checkSameInterval, out bool sameInterval ) { Series nullSeries = null; return GetPointsInterval(seriesList, isLogarithmic, logarithmBase, checkSameInterval, out sameInterval, out nullSeries); } /// /// This method returns minimum interval between /// any two data points from specified series. /// /// List of series. /// Indicates logarithmic scale. /// Logarithm Base /// True if check for the same interval should be performed. /// Return true if interval is the same. /// Series with the smallest interval between points. /// Minimum Interval internal double GetPointsInterval( List seriesList, bool isLogarithmic, double logarithmicBase, bool checkSameInterval, out bool sameInterval, out Series series ) { long ticksInterval = long.MaxValue; int monthsInteval = 0; double previousInterval = double.MinValue; double oldInterval = Double.MaxValue; // Initialize return value sameInterval = true; series = null; // Create comma separate string of series names string seriesNames = ""; if(seriesList != null) { foreach( string serName in seriesList ) { seriesNames += serName + ","; } } // Do not calculate interval every time; if( checkSameInterval == false || diffIntervalAlignmentChecked == true) { if (!isLogarithmic) { if( !double.IsNaN(intervalData) && _intervalSeriesList == seriesNames) { sameInterval = intervalSameSize; series = _intervalSeries; return intervalData; } } else { if( !double.IsNaN(intervalLogData) && _intervalSeriesList == seriesNames) { sameInterval = intervalSameSize; series = _intervalSeries; return intervalLogData; } } } // Data series loop int seriesIndex = 0; Series currentSmallestSeries = null; ArrayList[] seriesXValues = new ArrayList[seriesList.Count]; foreach( string ser in seriesList ) { Series dataSeries = Common.DataManager.Series[ ser ]; bool isXValueDateTime = dataSeries.IsXValueDateTime(); // Copy X values to array and prepare for sorting Sort X values. seriesXValues[seriesIndex] = new ArrayList(); bool sortPoints = false; double prevXValue = double.MinValue; double curentXValue = 0.0; if(dataSeries.Points.Count > 0) { if (isLogarithmic) { prevXValue = Math.Log(dataSeries.Points[0].XValue, logarithmicBase); } else { prevXValue = dataSeries.Points[0].XValue; } } foreach( DataPoint point in dataSeries.Points ) { if (isLogarithmic) { curentXValue = Math.Log(point.XValue, logarithmicBase); } else { curentXValue = point.XValue; } if(prevXValue > curentXValue) { sortPoints = true; } seriesXValues[seriesIndex].Add(curentXValue); prevXValue = curentXValue; } // Sort X values if(sortPoints) { seriesXValues[seriesIndex].Sort(); } // Data point loop for( int point = 1; point < seriesXValues[seriesIndex].Count; point++ ) { // Interval between two sorted data points. double interval = Math.Abs( (double)seriesXValues[seriesIndex][ point - 1 ] - (double)seriesXValues[seriesIndex][ point ] ); // Check if all intervals are same if(sameInterval) { if(isXValueDateTime) { if(ticksInterval == long.MaxValue) { // Calculate first interval GetDateInterval( (double)seriesXValues[seriesIndex][ point - 1 ], (double)seriesXValues[seriesIndex][ point ], out monthsInteval, out ticksInterval); } else { // Calculate current interval long curentTicksInterval = long.MaxValue; int curentMonthsInteval = 0; GetDateInterval( (double)seriesXValues[seriesIndex][ point - 1 ], (double)seriesXValues[seriesIndex][ point ], out curentMonthsInteval, out curentTicksInterval); // Compare current interval with previous if(curentMonthsInteval != monthsInteval || curentTicksInterval != ticksInterval) { sameInterval = false; } } } else { if( previousInterval != interval && previousInterval != double.MinValue ) { sameInterval = false; } } } previousInterval = interval; // If not minimum interval keep the old one if( oldInterval > interval && interval != 0) { oldInterval = interval; currentSmallestSeries = dataSeries; } } ++seriesIndex; } // If interval is not the same check if points from all series are aligned this.diffIntervalAlignmentChecked = false; if( checkSameInterval && !sameInterval && seriesXValues.Length > 1) { bool sameXValue = false; this.diffIntervalAlignmentChecked = true; // All X values must be same int listIndex = 0; foreach(ArrayList xList in seriesXValues) { for(int pointIndex = 0; pointIndex < xList.Count && !sameXValue; pointIndex++) { double xValue = (double)xList[pointIndex]; // Loop through all other lists and see if point is there for(int index = listIndex + 1; index < seriesXValues.Length && !sameXValue; index++) { if( (pointIndex < seriesXValues[index].Count && (double)seriesXValues[index][pointIndex] == xValue) || seriesXValues[index].Contains(xValue)) { sameXValue = true; break; } } } ++listIndex; } // Use side-by-side if at least one xommon X value between eries found if(sameXValue) { sameInterval = true; } } // Interval not found. Interval is 1. if( oldInterval == Double.MaxValue) { oldInterval = 1; } intervalSameSize = sameInterval; if (!isLogarithmic) { intervalData = oldInterval; _intervalSeries = currentSmallestSeries; series = _intervalSeries; _intervalSeriesList = seriesNames; return intervalData; } else { intervalLogData = oldInterval; _intervalSeries = currentSmallestSeries; series = _intervalSeries; _intervalSeriesList = seriesNames; return intervalLogData; } } /// /// Calculates the difference between two values in years, months, days, ... /// /// First value. /// Second value. /// Interval in months. /// Interval in ticks. private void GetDateInterval(double value1, double value2, out int monthsInteval, out long ticksInterval) { // Convert values to dates DateTime date1 = DateTime.FromOADate(value1); DateTime date2 = DateTime.FromOADate(value2); // Calculate months difference monthsInteval = date2.Month - date1.Month; monthsInteval += (date2.Year - date1.Year) * 12; // Calculate interval in ticks for days, hours, ... ticksInterval = 0; ticksInterval += (date2.Day - date1.Day) * TimeSpan.TicksPerDay; ticksInterval += (date2.Hour - date1.Hour) * TimeSpan.TicksPerHour; ticksInterval += (date2.Minute - date1.Minute) * TimeSpan.TicksPerMinute; ticksInterval += (date2.Second - date1.Second) * TimeSpan.TicksPerSecond; ticksInterval += (date2.Millisecond - date1.Millisecond) * TimeSpan.TicksPerMillisecond; } #endregion } }