پیاده سازی یک کنترل Chart در #C

یکشنبه 13 دی 1394

در این مقاله قصد داریم چارتی را پیاده سازی کنیم که قابلیت نمایش چند نمودار را همزمان داشته باشد. قابلیت انعطاف پذیر ی داشته باشد و نمودارها بدون در نظر گرفتن محدوده چارت با آن تناسب داشته باشند. از کنترل چارت مایکروسافت استفاده خواهیم کرد و با توجه به نیازهای خود، قابلیت ها و امکانات آن را گسترش خواهیم داد.

پیاده سازی یک کنترل Chart در #C

در این مقاله قصد داریم چارتی را پیاده سازی کنیم که قابلیت نمایش چند نمودار را همزمان داشته باشد. قابلیت انعطاف پذیر ی داشته باشد و نمودارها  بدون در نظر گرفتن محدوده چارت با آن تناسب داشته باشند. از کنترل چارت مایکروسافت استفاده خواهیم کرد و با توجه به نیازهای خود، قابلیت ها و امکانات آن را گسترش خواهیم داد.

مشکل با نمودارهای دیگر این است که به محدوده های بسیار متفاوتی تمایل دارند، که ترکیب آنها را در یک چارت مشکل می سازد. سایز کنترل نمودار قابل متناسب با همه نمودار ها تغییر میکند ،  یک گراف که محدوده ای بین 1 تا 100 دارد می تواند گراف دیگری که محدوده ای بین 0.001 تا 0.01 دارد به صورت مجازی ایجاد کند. اما اگر بخواهیم دو گراف را بدون درنظر گرفتن محدوده شان مقایسه کنیم چه می شود؟

از این رو به یک محدوده چارت برای هر گراف  یا گراف هایی که می توانند دسته بندی شوند نیاز داریم. این محدوده های چارت انباشته می شوند و هر کدام محور ها و سایز خود را بر اساس گرافی که نمایش داده می شود دارند. نتیجه یک چارت است که گراف های خود را دارد که متناسب با محدوده چارت قرار میگیرند.

چارت مجموعه ای از محدوده های نموداری مختلف است که کنار هم انباشته شده اند. برای هر گراف تک سه محدوده چارت مختلف نیاز داریم. یکی برای نمایش گراف ، یکی برای محور  X و دیگری برای محور  Y . این محدوده های چارت با یک شماره   GUID  به یکدیگر مقید می شوند.

 از آنجا که چارت اصلی  NET.  از این قابلیت پشتیبانی نمیکند، هر محدوده چارت را با استفاده از کلاس  ChartAreaTagger برچسب زده ایم.

/// <summary>
/// We use this class to tag our chart areas.
/// </summary>
private class ChartAreaTagger
{
    public ChartAreaTagger(Guid _guid, ChartAreaType _chartAreaType, Color _chartColor)
    {
        this.guid = _guid;
        this.chartAreaType = _chartAreaType;
        this.chartColor = _chartColor;
    }

    private Guid guid;
    /// <summary>
    /// This is the unique value that binds all three chart areas together (X, Y and Chart).
    /// </summary>
    public Guid @Guid
    {
        get { return guid; }
    }

    private ChartAreaType chartAreaType;
    /// <summary>
    /// This defines which type of chart area are we dealing with.
    /// </summary>
    public ChartAreaType @ChartAreaType
    {
        get { return chartAreaType; }
    }

    private Color chartColor;
    public Color ChartColor
    {
        get { return chartColor; }
    }
}

/// <summary>
/// This enumerates the three possible chart area types.
/// </summary>
private enum ChartAreaType
{
    /// <summary>
    /// This is for X axis labels.
    /// </summary>
    AXIS_X = 2,

    /// <summary>
    /// This is for Y axis labels.
    /// </summary>
    AXIS_Y = 4,

    /// <summary>
    /// This is for displaying our chart.
    /// </summary>
    CHART = 8
}

کلاس  ChartAreaTagger همه اطلاعات لازم برای قرار دادن گراف های مختلف را بر روی چارت و اجرای  Layout  فراهم میکند.

عنصر  GUI

چارت یک  UserControl را ارث بری میکند که یه شیء دارد : کنترل چارتNET.   . کنترل یک محدوده چارت و یک نقش اضافه شده در طراحی دارد. این دو شیء هیچگاه تغییر نمیکنند.

برای حذف و اضافه کردن محدوده های چارت در صورت نیاز ،باید فرمت مشخصی در طراحی داشته باشیم، برای انجام این کار  نیاز به یک متد در کد خود داریم که همه چارت ها فرمت مشخصی داشته باشند:

/// The routine to format our chart areas (all are formatted the same).
/// </summary>
private void FormatChartArea(ChartArea _chartArea)
{
    ...
}

 محدوده چارت اصلی که برای نمایش grid  و نگهداری گراف ها استفاده می شود ، به بعضی استثناها نیاز دارد که به صورت زیر متد  Load  را برای آن مینویسم :

this.chart1.ChartAreas[0].AxisX.LabelStyle.Enabled = false;
this.chart1.ChartAreas[0].AxisX.MajorTickMark.Enabled = false;
this.chart1.ChartAreas[0].AxisX.LineColor = Color.Transparent;
this.chart1.ChartAreas[0].AxisY.LabelStyle.Enabled = false;
this.chart1.ChartAreas[0].AxisY.MajorTickMark.Enabled = false;
this.chart1.ChartAreas[0].AxisY.LineColor = Color.Transparent;

اضافه کردن یک گراف

اضافه کردن یک گراف با چک کردن اینکه آیا یک محدوده مناسب در چارت موجود است یا نه شروع می شود. این کار را با استفاده از بدست آوردن حداقل و حداکثر چارت جدید و مقایسه آن با چارت موجود انجام می دهیم .

//obtain minimums and maximums...
float _minX = this.GetMinX(_points);
float _maxX = this.GetMaxX(_points);
float _minY = this.GetMinY(_points);
float _maxY = this.GetMaxY(_points);

//...so we can obtain the right chart areas
ChartArea _chartAreaAxisX = null;
ChartArea _chartAreaAxisY = null;
ChartArea _chartAreaChart = this.GetBestSuitedChartArea(
    _minX, _maxX,
    _minY, _maxY,
    out _chartAreaAxisX, out _chartAreaAxisY);

ما تفاوت دو چارت را به صورت درصدی حساب میکنیم ، ممکن است تفاوت حساب شده مقدار بزرگی شود در صورتی که با مقادیر کمی این تفاوت حساب شده باشد.  به طور مثال دو گراف را در نظر بگیرید که یکی 0 تا 100 و دیگری 1 تا 100 باشد . گراف در منطقه چارت مشابه قرار خواهد گرفت. اما تفاوت درصدی آنها برای 0 تا 1 ، 100 % خواهد بود. از آنجایی که این مقدار بیشتر از  MAX_PERCENTAL_DIFFERENCE است که برای 10% تنظیم می شود ، گراف در محدوده چارتی خود قرار خواهد گرفت. 

برای مقابله با این موضوع ، تفاوت درصدی در گراف ها را بررسی میکنیم. اگر مینیمم ، ماکسیمم و تناسب آنها خارج از MAX_PRECENTRAL_DIFFERENCE بود ، آنگاه محدوده چارت مناسب نخواهد بود.

/// <summary>
/// Routine for obtaining the best suited chart area from the already present one, 
/// based on the given chart proportions.
/// </summary>
private ChartArea GetBestSuitedChartArea(
    float _minX, float _maxX,
    float _minY, float _maxY,
    out ChartArea _suitableAxisX, out ChartArea _suitableAxisY)
{
    List<ChartArea> _suitableAxisXtmp = new List<ChartArea>();
    List<ChartArea> _suitableAxisYtmp = new List<ChartArea>();

    ChartArea _suitableChartArea = null;

    foreach (ChartArea _chartArea in this.chart1.ChartAreas)
    {
        //just a quick check if this chart area is even worth considering
        if (double.IsNaN(_chartArea.AxisX.Minimum))
        {
            continue;
        }
        if (double.IsInfinity(_chartArea.AxisX.Maximum))
        {
            continue;
        }
        if (double.IsNaN(_chartArea.AxisY.Minimum))
        {
            continue;
        }
        if (double.IsInfinity(_chartArea.AxisY.Maximum))
        {
            continue;
        }

        if (_chartArea.Tag is ChartAreaTagger)
        {
            ChartAreaTagger _chartAreaTagger = (ChartAreaTagger)_chartArea.Tag;

            #region "for X axis"
            if (this.GetPercentageDifference
            (_chartArea.AxisX.Minimum, _minX) > MAX_PERCENTAGE_DIFFERENCE)
            {
                if ((this.GetPercentageDifference(_chartArea.AxisX.Maximum - _chartArea.AxisX.Minimum, 
                	_maxX - _minX) > MAX_PERCENTAGE_DIFFERENCE)
                    || (this.GetPercentageDifference(_chartArea.AxisX.Maximum, _maxX) 
                    	> MAX_PERCENTAGE_DIFFERENCE))
                {
                    continue;
                }
            }
            if (this.GetPercentageDifference
		(_chartArea.AxisX.Maximum, _maxX) > MAX_PERCENTAGE_DIFFERENCE)
            {
                if ((this.GetPercentageDifference(_chartArea.AxisX.Maximum - 
                	_chartArea.AxisX.Minimum, _maxX - _minX) > MAX_PERCENTAGE_DIFFERENCE)
                    || (this.GetPercentageDifference
                    (_chartArea.AxisX.Minimum, _minX) > MAX_PERCENTAGE_DIFFERENCE))
                {
                    continue;
                }
            }

            if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_X)
            {
                _suitableAxisXtmp.Add(_chartArea);
            }
            #endregion "for X axis"

            #region "for Y axis"
            if (this.GetPercentageDifference(_chartArea.AxisY.Minimum, _minY) 
            	> MAX_PERCENTAGE_DIFFERENCE)
            {
                if ((this.GetPercentageDifference(_chartArea.AxisY.Maximum - 
                	_chartArea.AxisY.Minimum, _maxY - _minY) > MAX_PERCENTAGE_DIFFERENCE)
                    || (this.GetPercentageDifference
                    (_chartArea.AxisY.Maximum, _maxY) > MAX_PERCENTAGE_DIFFERENCE))
                {
                    continue;
                }
            }
            if (this.GetPercentageDifference
            (_chartArea.AxisY.Maximum, _maxY) > MAX_PERCENTAGE_DIFFERENCE)
            {
                if ((this.GetPercentageDifference(_chartArea.AxisY.Maximum - 
                _chartArea.AxisY.Minimum, _maxY - _minY) > MAX_PERCENTAGE_DIFFERENCE)
                    || (this.GetPercentageDifference
                    (_chartArea.AxisY.Minimum, _minY) > MAX_PERCENTAGE_DIFFERENCE))
                {
                    continue;
                }
            }

            if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_Y)
            {
                _suitableAxisYtmp.Add(_chartArea);
            }
            #endregion "for Y axis"

            //if we are here, that means that both X and Y axes are suitable, 
            //so the chart area is suitable as well.
            if (_chartAreaTagger.ChartAreaType == ChartAreaType.CHART)
            {
                _suitableChartArea = _chartArea;
            }
        }
    }

    //when there are many suitable areas, we take the first ones.
    if (_suitableAxisXtmp.Count > 0)
    {
        _suitableAxisX = _suitableAxisXtmp[0];
    }
    else
    {
        _suitableAxisX = null;
    }
    if (_suitableAxisYtmp.Count > 0)
    {
        _suitableAxisY = _suitableAxisYtmp[0];
    }
    else
    {
        _suitableAxisY = null;
    }

    return _suitableChartArea;
}

اگر هیچ محدوده چارت مناسبی نبود ، آنگاه :

     . یک جدید ایجاد میکنیم.

     . آن را فرمت دهی میکنیم.

     . اندازه ابعاد آن را با توجه به گراف تنظیم میکنیم.

Guid _guid = Guid.NewGuid();

if ((_chartAreaAxisX == null) || (_chartAreaAxisY == null))
{
    #region "one for showing the chart"
    _chartAreaChart = new ChartArea(Guid.NewGuid().ToString());
    this.FormatChartArea(_chartAreaChart);
    _chartAreaChart.Tag = new ChartAreaTagger(_guid, ChartAreaType.CHART, _color);
    _chartAreaChart.BackColor = Color.Transparent;
    _chartAreaChart.IsSameFontSizeForAllAxes = this.chart1.ChartAreas[0].IsSameFontSizeForAllAxes;
    this.chart1.ChartAreas.Add(_chartAreaChart);

    //format for chart (no labels)
    this.FormatAxis(_chartAreaChart.AxisX,
            _minX, _maxX,
            false, true, false);
    this.FormatAxis(_chartAreaChart.AxisY,
        _minY, _maxY,
        false, true, false);
    #endregion "one for showing the chart"
}
if (_chartAreaAxisX == null)
{
    #region "one for X axis"
    _chartAreaAxisX = new ChartArea(Guid.NewGuid().ToString());
    this.FormatChartArea(_chartAreaAxisX);
    _chartAreaAxisX.BackColor = Color.Transparent;
    _chartAreaAxisX.IsSameFontSizeForAllAxes = this.chart1.ChartAreas[0].IsSameFontSizeForAllAxes;
    _chartAreaAxisX.Tag = new ChartAreaTagger(_guid, ChartAreaType.AXIS_X, _color);
    _chartAreaAxisX.AxisX.LabelStyle.ForeColor = _color;
    this.chart1.ChartAreas.Add(_chartAreaAxisX);

    //format for X axis
    this.FormatAxis(_chartAreaAxisX.AxisX,
        _minX, _maxX,
        true, false, true);

    //this is dummy
    this.FormatAxis(_chartAreaAxisX.AxisY,
        _minY, _maxY,
        false, false, false);
    #endregion "one for X axis"
}
if (_chartAreaAxisY == null)
{
    #region "one for Y axis"
    _chartAreaAxisY = new ChartArea(Guid.NewGuid().ToString());
    this.FormatChartArea(_chartAreaAxisY);
    _chartAreaAxisY.BackColor = Color.Transparent;
    _chartAreaAxisY.IsSameFontSizeForAllAxes = this.chart1.ChartAreas[0].IsSameFontSizeForAllAxes;
    _chartAreaAxisY.Tag = new ChartAreaTagger(_guid, ChartAreaType.AXIS_Y, _color);
    _chartAreaAxisY.AxisY.LabelStyle.ForeColor = _color;
    this.chart1.ChartAreas.Add(_chartAreaAxisY);

    //this is dummy
    this.FormatAxis(_chartAreaAxisY.AxisX,
            _minX, _maxX,
            false, false, false);

    //format for Y axis
    this.FormatAxis(_chartAreaAxisY.AxisY,
        _minY, _maxY,
        true, false, true);
    #endregion "one for Y axis"
}

اگربتوانیم یک محدوده چارت متناسب که با مینیمم و ماکسیمم ابعاد همخوانی داشته باشد پیدا کنیم، آنگاه :

     . چارت جدید خود را در آن قرار می دهیم .

     . تغییر اندازه محدوده چارت که پس از آن می توانید هر دو نمودار را در آن داشته باشید.

#region "check minimums and maximums of the eventual existing charts"
IEnumerable<PointF>[] _functionsOnChartArea = this.GetFunctionsOnChartArea(_chartAreaChart);
foreach (IEnumerable<PointF> _functionTmp in _functionsOnChartArea)
{
    float _functionMinX = this.GetMinX(_functionTmp);
    if (_functionMinX < _minX)
    {
        _minX = _functionMinX;
    }

    float _functionMaxX = this.GetMaxX(_functionTmp);
    if (_functionMaxX > _maxX)
    {
        _maxX = _functionMaxX;
    }

    float _functionMinY = this.GetMinY(_functionTmp);
    if (_functionMinY < _minY)
    {
        _minY = _functionMinY;
    }

    float _functionMaxY = this.GetMaxY(_functionTmp);
    if (_functionMaxY > _maxY)
    {
        _maxY = _functionMaxY;
    }
}
#endregion "check minimums and maximums of the eventual existing charts"

#region "and adjust the axes accordingly, so they can take the old chart plus the newly added"
this.FormatAxis(_chartAreaAxisX.AxisX,
        _minX, _maxX,
        true, false, true);
this.FormatAxis(_chartAreaAxisX.AxisY,
    _minY, _maxY,
    false, false, false);

this.FormatAxis(_chartAreaAxisY.AxisX,
        _minX, _maxX,
        false, false, false);
this.FormatAxis(_chartAreaAxisY.AxisY,
    _minY, _maxY,
    true, false, true);

this.FormatAxis(_chartAreaChart.AxisX,
    _minX, _maxX,
    false, true, false);
this.FormatAxis(_chartAreaChart.AxisY,
    _minY, _maxY,
    false, true, false);
#endregion "and adjust the axes accordingly, so they can take the old chart plus the newly added"

#region "then draw our chart"
//set the color to transparent, because we don't want chart to be shown on this axis
this.DrawGraph(_chartAreaAxisX, _points, null, Color.Transparent);

//set the color to transparent, because we don't want chart to be shown on this axis
this.DrawGraph(_chartAreaAxisY, _points, null, Color.Transparent);

//finally, add the chart and draw it
this.DrawGraph(_chartAreaChart, _points, _legendTitle, _color);
#endregion "then draw our chart"

 روش ذکر شده باید برای محور  X و  Y و چارت انجام شود.

چارت در نهایت کشیده شده است و تنها به قدم آخر که  Layout  است نیاز داریم.

 تنها چیزی مهم این است که با توجه به گراف ها می توان محدوده های بسیار متفاوتی وجود داشته داشته باشد و باید همه چیز را بر اساس در صد در نظر بگیریم.

برای درستی وضعیت محور های اضافی برچسبی را برای آنها در نظر میگیریم.  برای محور  X برچسب  Height  و برای محور  Y برچسب  Width را در نظر گرفتیم. بدیت ترتیب طرح وضعیت چارت اضافی را محاسبه کرده ایم که هر کدام توسط سایز برچسب تعیین می شوند.

private void PerformChartLayout()
{
    if (this.chart1.Series.Count == 0)
    {
        //hide the base chart area
        this.chart1.ChartAreas[0].Visible = false;
    }
    else
    {
        //make the base chart area visible
        this.chart1.ChartAreas[0].Visible = true;

        //we must have a simple graph on our base series, otherwise the grid 
        //would be hidden because of the MS chart's logic (but we make it transparent)
        if (!this.chart1.Series.Any(delegate (Series _series)
            {
                if (_series.ChartArea == this.chart1.ChartAreas[0].Name)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }))
        {
            //as we set our grid in percentages, the graph must be of suitable range
            this.DrawGraph(
                this.chart1.ChartAreas[0],
                new PointF[]
                {
            new PointF(1f, 1f),
            new PointF(100f, 100f)
                },
                null,
                Color.Transparent);
        }

        float _offsetX = 0;
        float _offsetY = 0;

        #region "position the chart plot positions with respect to label sizes"
        //we do this reversed, so the most recently added charts expand to outside
        foreach (ChartArea _chartArea in this.chart1.ChartAreas.Reverse())
        {
            //set the whole chart area position
            _chartArea.Position.FromRectangleF(this.ChartPosition);

            if (_chartArea.Tag is ChartAreaTagger)
            {
                ChartAreaTagger _chartAreaTagger = (ChartAreaTagger)_chartArea.Tag;

                if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_X)
                {
                    //set the size of the tick marks
                    _chartArea.AxisX.MajorTickMark.Size = this.TickMarkSizePercentageY;

                    //then measure the height of the labels
                    float _axisXLabelHeight = this.GetAxisXLabelHeightPercentage
                    	(_chartArea) + this.TickMarkSizePercentageY;

                    //and compute the position of the chart plot position 
                    //based on the size of the label and the offset, 
		    //which increases for every chart area
                    RectangleF _chartInnerPlotPosition = this.GetChartInnerPlotPosition();
                    _chartInnerPlotPosition.Y -= (_axisXLabelHeight + _offsetY);
                    _chartInnerPlotPosition.Height -= (_axisXLabelHeight + _offsetY);
                    _chartArea.InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition);

                    //increase the offset!
                    _offsetY += _axisXLabelHeight;
                }
                else if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_Y)
                {
                    //set the size of the tick marks
                    _chartArea.AxisY.MajorTickMark.Size = this.TickMarkSizePercentageX;

                    //then measure the width of the labels
                    float _axisYLabelWidth = this.GetAxisYLabelWidthPercentage
                    	(_chartArea) + this.TickMarkSizePercentageX;

                    //and compute the position of the chart plot position 
                    //based on the size of the label and the offset, 
		    //which increases for every chart area
                    RectangleF _chartInnerPlotPosition = this.GetChartInnerPlotPosition();
                    _chartInnerPlotPosition.X += (_axisYLabelWidth + _offsetX);
                    _chartInnerPlotPosition.Width -= (_axisYLabelWidth + _offsetX);
                    _chartArea.InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition);

                    //increase the offset!
                    _offsetX += _axisYLabelWidth;
                }
            }
        }
        #endregion "position the chart plot positions with respect to label sizes"

        //the chart areas are now positioned accordingly to the label sizes, 
        //but this is not enough; we must position them also by the full offsets 
        //(which we computed while positioning them),
        //so we just iterate through chart areas once again and use the computed offsets. 
        //The areas will thus start from offset and not from zero.
        #region "position the areas with respect to the offset"
        foreach (ChartArea _chartArea in this.chart1.ChartAreas)
        {
            //set the whole chart area position
            _chartArea.Position.FromRectangleF(this.ChartPosition);

            if (_chartArea.Tag is ChartAreaTagger)
            {
                ChartAreaTagger _chartAreaTagger = (ChartAreaTagger)_chartArea.Tag;

                if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_X)
                {
                    //this moves to the right of the screen and decreases in width
                    RectangleF _chartInnerPlotPosition = _chartArea.InnerPlotPosition.ToRectangleF();
                    _chartInnerPlotPosition.X += _offsetX;
                    _chartInnerPlotPosition.Width -= _offsetX;
                    _chartArea.InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition);
                }
                else if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_Y)
                {
                    //this doesn't move, but decreases in height
                    RectangleF _chartInnerPlotPosition = _chartArea.InnerPlotPosition.ToRectangleF();
                    _chartInnerPlotPosition.Height -= (_offsetY * 2);
                    _chartArea.InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition);
                }
                else if (_chartAreaTagger.ChartAreaType == ChartAreaType.CHART)
                {
                    //this moves to the right of the screen 
                    //(while not moving by Y), decreases in width and in height
                    RectangleF _chartInnerPlotPosition = this.GetChartInnerPlotPosition();
                    _chartInnerPlotPosition.X += _offsetX;
                    _chartInnerPlotPosition.Width -= _offsetX;
                    _chartInnerPlotPosition.Height -= (_offsetY * 2);
                    _chartArea.InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition);
                }
            }
            else if (_chartArea == this.chart1.ChartAreas[0]) //don't forget the base chart area, 
            	//which also must take the offset into account
            {
                //this does the same as the other CHART chart areas
                RectangleF _chartInnerPlotPosition = this.GetChartInnerPlotPosition();
                _chartInnerPlotPosition.X += _offsetX;
                _chartInnerPlotPosition.Width -= _offsetX;
                _chartInnerPlotPosition.Height -= (_offsetY * 2);
                this.chart1.ChartAreas[0].InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition);
            }
        }
        #endregion "position the areas with respect to the offset"
    }
}

از متد  PrrformLayout() جهت اطمینان از آنکه همیشه یک  گراف در محدوده چارت پایه وجود دارد ، به طوری که  grid  نمایش داده خواهد شد.

آموزش سی شارپ

فایل های ضمیمه

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان
  • C#.net
  • 2k بازدید
  • 1 تشکر

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید