#region << Using Directives >>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Threading;
using System.Windows.Forms;
using System.Diagnostics;
using Volpe.Cafe.Utils;
#endregion

namespace Volpe.Cafe.EspressoUI.Charts
{
    //
    // This file contains implementation of the following Nested Types:
    //  - GridInfo         -- supports layout and painting of the chart's grid area
    //  - LegendInfo       -- supports layout and painting of the chart's legend (iterates DataSeries object)
    //  - AxisInfo         -- supports layout and painting of the category and primary/secondary values axis
    //  - SeriesInfo       -- supports layout and painting of the data series (iterates DataSeries object)
    //  - AnimationEffects -- supports animation effects and painting elements for the chart
    //
    // Additionally, this file contains implementation of the Provided Properties for each of the Nested Types.
    //
    partial class Chart
    {

        #region /*** BaseInfo base class ***/

        /// <summary>
        /// Provides the base class for implementing various chart elements.
        /// </summary>
        [Serializable]
        protected abstract class BaseInfo
        {

            #region /*** Events ***/

            /// <summary>Occurs before updating the object's layout area.</summary>
            public event EventHandler LayoutUpdating;
            /// <summary>Occurs after the object's layout area has been updated.</summary>
            public event EventHandler LayoutUpdated;

            #endregion

            #region /*** Ctors ***/

            internal BaseInfo(Chart parent)
            {
                this.Parent  = parent;
                this.IsDirty = true;
            }

            #endregion

            #region /*** Methods ***/

            /// <summary>
            /// Raises the <see cref="LayoutUpdating"/> event.
            /// </summary>
            protected void OnLayoutUpdating()
            {
                this.LayoutUpdating?.Invoke(this, EventArgs.Empty);
            }
            /// <summary>
            /// Raises the <see cref="LayoutUpdated"/> event.
            /// </summary>
            protected void OnLayoutUpdated()
            {
                this.LayoutUpdated?.Invoke(this, EventArgs.Empty);
            }

            /// <summary>
            /// Invalidates the parent control, without updating the object's layout area before repainting.
            /// </summary>
            protected void Invalidate()
            {
                this.Parent.Invalidate();
            }
            /// <summary>
            /// Sets the object's layout area as dirty, optionally forcing the parent control to immediately invalidate its client
            /// area and redraw itself.
            /// </summary>
            /// <param name="invalidateParent">true, to invalidate the parent control; false, otherwise.</param>
            public void SetDirty(bool invalidateParent)
            {
                this.IsDirty = true;
                if (invalidateParent)
                {
                    this.Parent.Invalidate();
                }
            }
            /// <summary>
            /// Updates the layout area of the object.
            /// </summary>
            public void UpdateLayout()
            {
                this.OnLayoutUpdating();
                this.UpdateLayoutInternal();
                this.IsDirty = false;
                this.OnLayoutUpdated();
            }
            /// <summary>
            /// Updates the layout area of the object.
            /// </summary>
            protected abstract void UpdateLayoutInternal();
            /// <summary>
            /// Paints the contents of this object on the chart.
            /// </summary>
            /// <param name="g">The graphics object to use for painting.</param>
            public void Paint(Graphics g)
            {
                if (this.IsDirty) { this.UpdateLayout(); }
                this.PaintInternal(g);
            }
            /// <summary>
            /// Paints the contents of this object on the chart.
            /// </summary>
            /// <param name="g">The graphics object to use for painting.</param>
            protected abstract void PaintInternal(Graphics g);

            /// <summary>
            /// Returns the color index to use for painting the series.
            /// </summary>
            /// <param name="seriesIndex">The index of the series within the data series collection to check.</param>
            /// <returns>The color index to use for painting the series.</returns>
            internal int GetColorIndex(int seriesIndex)
            {
                int colorIndex = this.Parent.DataSeries[seriesIndex].ColorIndex;
                return (colorIndex == -1) ? seriesIndex : colorIndex;
            }

            #endregion

            #region /*** Properties ***/

            /// <summary>Gets the parent of this object.</summary>
            public Chart Parent { get; private set; }
            /// <summary>Gets whether the layout area has been modified since the last paint operation.</summary>
            public bool IsDirty { get; private set; }
            /// <summary>Gets the color palette associated with this chart.</summary>
            protected ColorPalette ColorPalette { get { return this.Parent.ColorPalette; } }

            #endregion

        }

        #endregion

        #region /*** GridInfo class and Provided Properties ***/

        //----- ChartBase.GridInfo class -----
        /// <summary>
        /// Handles layout and painting of the chart's grid.
        /// </summary>
        [Serializable]
        protected class GridInfo : BaseInfo
        {

            #region /*** Ctors ***/

            internal GridInfo(Chart parent) : base(parent) { }

            #endregion

            #region /*** Methods ***/

            /// <summary>
            /// Updates the layout area of the chart's grid.
            /// </summary>
            protected override void UpdateLayoutInternal()
            {
                Padding pad = this.GridPadding;
                int x = pad.Left;
                int y = (this.Parent.ShowTitle) ? (int)this.Parent.TitleHeight : pad.Top;
                int w =  this.Parent.Width  - x - pad.Right;
                int h =  this.Parent.Height - y - pad.Bottom;
                this.Bounds = new Rectangle(x, y, w, h);
                this.GridSize = (int)Math.Round(this.Parent.AltScaleFactor) * 8;
                this.Ticks  = new Point(w / this.GridSize, h / this.GridSize);
            }
            /// <summary>
            /// Paints the grid on the chart.
            /// </summary>
            /// <param name="g">The graphics object to use for painting.</param>
            protected override void PaintInternal(Graphics g)
            {
                // paint back color
                using (Brush b = new SolidBrush(this.BackColor))
                {
                    g.FillRectangle(b, this.Bounds);
                }
                // paint grid lines
                if (this.ShowGridLines)
                {
                    using (Pen p = new Pen(this.GridLinesColor))
                    {
                        int x = this.Bounds.X;
                        int y = this.Bounds.Y;
                        int w = this.Bounds.Width;
                        int h = this.Bounds.Height;
                        //
                        for (int i = this.GridSize; i < w; i += this.GridSize)
                        {
                            g.DrawLine(p, x + i, y + 1, x + i, y + h - 1);
                        }
                        if (this.Orientation == GridOrientation.Flipped || this.Orientation == GridOrientation.SwappedAndFlipped)
                        {
                            for (int i = this.GridSize; i < h; i += this.GridSize)
                            {
                                g.DrawLine(p, x + 1, y + i, x + w - 1, y + i);
                            }
                        }
                        else
                        {
                            for (int i = h - this.GridSize; i > 0; i -= this.GridSize)
                            {
                                g.DrawLine(p, x + 1, y + i, x + w - 1, y + i);
                            }
                        }
                    }
                }
                // paint the grid border
                if (!this.HasBorder)
                {
                    if (this.ShowGridLines)
                    {
                        ControlPaint.DrawBorder(g, this.Bounds, this.GridLinesColor, ButtonBorderStyle.Solid);
                    }
                }
                else if (this.BorderStyle == Border3DStyle.Flat || this.Parent.Animation.IsRotate || this.Parent.Animation.IsRotate3D)
                {
                    ControlPaint.DrawBorder(g, this.Bounds, this.BorderColor, ButtonBorderStyle.Solid);
                }
                else
                {
                    ControlPaint.DrawBorder3D(g, this.Bounds, this.BorderStyle);
                }
            }

            #endregion

            #region /*** Properties ***/

            //----- provided properties -----
            internal Padding GridPadding
            {
                get { return this._gridPadding; }
                set
                {
                    //value = value / 8 * 8;
                    if (this._gridPadding != value)
                    {
                        this._gridPadding = value;
                        this.SetDirty(true);
                        this.Parent.OnGridPaddingChanged();
                    }
                }
            }
            internal Border3DStyle BorderStyle
            {
                get { return this._borderStyle; }
                set
                {
                    if (this._borderStyle != value)
                    {
                        this._borderStyle = value;
                        this.Invalidate();
                        this.Parent.OnGridBorderStyleChanged();
                    }
                }
            }
            internal Color BorderColor
            {
                get { return this._borderColor; }
                set
                {
                    if (this._borderColor != value)
                    {
                        this._borderColor = value;
                        this.Invalidate();
                        this.Parent.OnGridBorderColorChanged();
                    }
                }
            }
            internal Color BackColor
            {
                get { return this._backColor; }
                set
                {
                    if (this._backColor != value)
                    {
                        this._backColor = value;
                        this.Invalidate();
                        this.Parent.OnGridBackColorChanged();
                    }
                }
            }
            internal bool ShowGridLines
            {
                get { return this._showGridLines; }
                set
                {
                    if (this._showGridLines != value)
                    {
                        this._showGridLines = value;
                        this.Invalidate();
                        this.Parent.OnShowGridLinesChanged();
                    }
                }
            }
            internal Color GridLinesColor
            {
                get { return this._gridLinesColor; }
                set
                {
                    if (this._gridLinesColor != value)
                    {
                        this._gridLinesColor = value;
                        this.Invalidate();
                        this.Parent.OnGridLinesColorChanged();
                    }
                }
            }
            internal GridOrientation Orientation
            {
                get { return this._orientation; }
                set
                {
                    if (this._orientation != value)
                    {
                        this._orientation = value;
                        this.SetDirty(true);
                        this.Parent.OnGridOrientationChanged();
                    }
                }
            }

            //----- read-only properties -----
            /// <summary>Gets whether the chart's grid area has a border. If <see cref="BorderStyle"/> is set to
            ///   <see cref="Border3DStyle.Adjust"/>, the grid area is assumed to have no border.</summary>
            public bool HasBorder { get { return this.BorderStyle != Border3DStyle.Adjust; } }
            /// <summary>Gets the rectangle that defines the bounds of the chart's grid.</summary>
            public Rectangle Bounds { get; private set; }
            /// <summary>Gets the size of each tick (or square) of the chart's grid.</summary>
            public int GridSize { get; private set; }
            /// <summary>Gets the number of grid ticks along the X- and Y-axis.</summary>
            public Point Ticks { get; private set; }

            /// <summary>Gets whether the grid orientation is flipped, having the (0,0) point located at the top-left corner of the grid.</summary>
            public bool IsFlipped { get { return this.Orientation == GridOrientation.Flipped; } }
            /// <summary>Gets whether the grid orientation is swapped, having the Category and Values axis swapped and the (0,0)
            ///   point located at the bottom-left corner of the grid.</summary>
            public bool IsSwapped { get { return this.Orientation == GridOrientation.Swapped; } }
            /// <summary>Gets whether the grid orientation swapped and flipped, having the Category and Values axis swapped and
            ///   the (0,0) point located at the top-left corner of the grid.</summary>
            public bool IsSwappedAndFlipped { get { return this.Orientation == GridOrientation.SwappedAndFlipped; } }

            #endregion

            #region /*** Variables ***/

            Padding         _gridPadding    = new Padding(8);
            Border3DStyle   _borderStyle    = Border3DStyle.Raised;
            Color           _borderColor    = Color.Black;
            Color           _backColor      = SystemColors.Control;
            bool            _showGridLines  = true;
            Color           _gridLinesColor = Color.LightGray;
            GridOrientation _orientation    = GridOrientation.Default;

            #endregion

        }

        //----- ChartBase.GridInfo instance -----
        /// <summary>Gets an object that handles layout and painting of the chart's grid.</summary>
        protected GridInfo Grid { get; private set; }

        //----- ChartBase.GridInfo Provided Properties and Events -----
        #region < Show/Hide Grid Provided Properties >
        /// <summary>Gets or sets the interior spacing between the chart's client area and the chart's grid.</summary>
        [Browsable(true)]
        [DefaultValue(typeof(Padding), "8")]
        [Category("Grid Appearance")]
        [Description("Specifies the interior spacing between the chart's client area and the chart's grid.")]
        public Padding GridPadding { get { return this.Grid.GridPadding; } set { this.Grid.GridPadding = value; } }
        /// <summary>Gets or set the border style of the chart's grid.</summary>
        [Browsable(true)]
        [DefaultValue(Border3DStyle.Raised)]
        [Category("Grid Appearance")]
        [Description("The border style of the chart's grid.")]
        public Border3DStyle GridBorderStyle { get { return this.Grid.BorderStyle; } set { this.Grid.BorderStyle = value; } }
        /// <summary>Gets or set the border color of the chart's grid.</summary>
        [Browsable(true)]
        [DefaultValue(typeof(Color), "Black")]
        [Category("Grid Appearance")]
        [Description("The border color of the chart's grid.")]
        public Color GridBorderColor { get { return this.Grid.BorderColor; } set { this.Grid.BorderColor = value; } }
        /// <summary>Gets or set the background color of the chart's grid.</summary>
        [Browsable(true)]
        [DefaultValue(typeof(Color), "Control")]
        [Category("Grid Appearance")]
        [Description("The background color of the chart's grid.")]
        public Color GridBackColor { get { return this.Grid.BackColor; } set { this.Grid.BackColor = value; } }
        /// <summary>Gets or sets whether to display the grid lines on the chart control.</summary>
        [Browsable(true)]
        [DefaultValue(true)]
        [Category("Grid Appearance")]
        [Description("Indicates whether to display the grid lines on the chart control.")]
        public bool ShowGridLines { get { return this.Grid.ShowGridLines; } set { this.Grid.ShowGridLines = value; } }
        /// <summary>Gets or sets the color of the chart's grid lines.</summary>
        [Browsable(true)]
        [DefaultValue(typeof(Color), "LightGray")]
        [Category("Grid Appearance")]
        [Description("The color of the chart's grid lines.")]
        public Color GridLinesColor { get { return this.Grid.GridLinesColor; } set { this.Grid.GridLinesColor = value; } }
        /// <summary>Gets or sets the chart's grid orientation.</summary>
        [Browsable(true)]
        [DefaultValue(GridOrientation.Default)]
        [Category("Grid Appearance")]
        [Description("Indicates the chart's grid orientation.")]
        public GridOrientation GridOrientation { get { return this.Grid.Orientation; } set { this.Grid.Orientation = value; } }
        #endregion
        #region < Show/Hide Grid Provided Property Events >
        /// <summary>Occurs when the chart's <see cref="GridPadding"/> property changes.</summary>
        public event EventHandler GridPaddingChanged;
        /// <summary>Occurs when the chart's <see cref="GridBorderStyle"/> property changes.</summary>
        public event EventHandler GridBorderStyleChanged;
        /// <summary>Occurs when the chart's <see cref="GridBorderColor"/> property changes.</summary>
        public event EventHandler GridBorderColorChanged;
        /// <summary>Occurs when the chart's <see cref="GridBackColor"/> property changes.</summary>
        public event EventHandler GridBackColorChanged;
        /// <summary>Occurs when the chart's <see cref="ShowGridLines"/> property changes.</summary>
        public event EventHandler ShowGridLinesChanged;
        /// <summary>Occurs when the chart's <see cref="GridLinesColor"/> property changes.</summary>
        public event EventHandler GridLinesColorChanged;
        /// <summary>Occurs when the chart's <see cref="GridOrientation"/> property changes.</summary>
        public event EventHandler GridOrientationChanged;
        #endregion
        #region < Show/Hide Grid methods for raising Provided Property Events >
        /// <summary>Raises the <see cref="GridPaddingChanged"/> event.</summary>
        protected virtual void OnGridPaddingChanged() { this.GridPaddingChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="GridBorderStyleChanged"/> event.</summary>
        protected virtual void OnGridBorderStyleChanged() { this.GridBorderStyleChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="GridBorderColorChanged"/> event.</summary>
        protected virtual void OnGridBorderColorChanged() { this.GridBorderColorChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="GridBackColorChanged"/> event.</summary>
        protected virtual void OnGridBackColorChanged() { this.GridBackColorChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="ShowGridLinesChanged"/> event.</summary>
        protected virtual void OnShowGridLinesChanged() { this.ShowGridLinesChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="GridLinesColorChanged"/> event.</summary>
        protected virtual void OnGridLinesColorChanged() { this.GridLinesColorChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="GridOrientationChanged"/> event.</summary>
        protected virtual void OnGridOrientationChanged() { this.GridOrientationChanged?.Invoke(this, EventArgs.Empty); }
        #endregion

        //----- ChartBase.GridInfo Read-Only Properties -----
        #region < Show/Hide Grid Read-Only Properties >
        /// <summary>Gets the rectangle that defines the bounds of the chart's grid.</summary>
        [Browsable(false)]
        protected internal Rectangle GridBounds { get { return this.Grid.Bounds; } }
        /// <summary>Gets the x-coordinate of the chart's grid.</summary>
        [Browsable(false)]
        protected internal int GridX { get { return this.Grid.Bounds.X; } }
        /// <summary>Gets the y-coordinate of the chart's grid.</summary>
        [Browsable(false)]
        protected internal int GridY { get { return this.Grid.Bounds.Y; } }
        /// <summary>Gets the width of the chart's grid.</summary>
        [Browsable(false)]
        protected internal int GridWidth { get { return this.Grid.Bounds.Width; } }
        /// <summary>Gets the height of the chart's grid.</summary>
        [Browsable(false)]
        protected internal int GridHeight { get { return this.Grid.Bounds.Height; } }
        /// <summary>Gets the size of each tick (or square) of the chart's grid.</summary>
        [Browsable(false)]
        protected internal int GridSize { get { return this.Grid.GridSize; } }
        /// <summary>Gets the number of grid ticks along the X-axis.</summary>
        [Browsable(false)]
        protected internal int GridTicksX { get { return this.Grid.Ticks.X; } }
        /// <summary>Gets the number of grid ticks along the Y-axis.</summary>
        [Browsable(false)]
        protected internal int GridTicksY { get { return this.Grid.Ticks.Y; } }
        #endregion

        #endregion

        #region /*** LegendInfo class and Provided Properties ***/

        //----- ChartBase.LegendInfo class -----
        /// <summary>
        /// Handles layout and painting of the chart's legend.
        /// </summary>
        [Serializable]
        protected class LegendInfo : BaseInfo
        {

            #region /*** Ctors ***/

            internal LegendInfo(Chart parent) : base(parent) { }

            #endregion

            #region /*** Methods ***/

            /// <summary>
            /// Updates the layout area of the chart's legend.
            /// </summary>
            protected override void UpdateLayoutInternal()
            {
                GridInfo grid = this.Parent.Grid;
                if (grid.IsDirty) { grid.UpdateLayout(); }
                int x = grid.Bounds.X;
                int y = grid.Bounds.Y;
                int w = grid.Bounds.Width;
                int h = grid.Bounds.Height;
                //
                List<DataSeries> dataSeries = this.Parent.DataSeries;
                Font             legendFont = this.Parent.ChartFontSm;
                float                 scale = this.Parent.ScaleFactor;
                float           lgnIconSize = Math.Max(4, grid.GridSize / 4);
                //
                int lblBreak = -1;
                float lgnW = 0, lgnH = 0, lgnX = 0, lgnY = 0;

                // update label sizes first
                this.UpdateLabelSizes();

                if (this.DockLeft || this.DockRight)
                {   // legend aligned vertically
                    for (int i = 0; i < dataSeries.Count; i++)
                    {
                        if (dataSeries[i].Visible)
                        {
                            SizeF sz = this.LabelSizes[i];
                            lgnW  = Math.Max(lgnW, sz.Width + lgnIconSize);           // leave room for legend icon
                            lgnH += Math.Max(lgnIconSize, sz.Height) + LegendPadding; // leave room for legend icon, and leave 4px spacing between items
                        }
                    }
                    // add 2px padding on each edge
                    lgnW += LegendPadding;
                    // compute legend position
                    lgnX = (this.DockLeft) ? x + LegendPadding : x + w - lgnW - LegendPadding;
                    //lgnY = y + grid.GridSize * 0.75F + LegendPadding;
                    lgnY = Math.Min(y + (h - lgnH) / 2F, y + grid.GridSize * 0.75F + scale * 8F + LegendPadding);
                }
                else
                {   // legend aligned horizontally
                    for (int i = 0; i < dataSeries.Count; i++)
                    {   // begin by computing max size on a single row
                        if (dataSeries[i].Visible)
                        {
                            SizeF sz = this.LabelSizes[i];
                            lgnW += sz.Width + LegendPadding + lgnIconSize;           // leave room for legend icon, and leave 4px spacing between items
                            lgnH  = Math.Max(lgnH, Math.Max(sz.Height, lgnIconSize)); // leave room for legend icon
                        }
                    }
                    // if legend's single-row approaches grid's width, recalc on two rows
                    if (lgnW + LegendPadding * 2 >= w)
                    {
                        float lgnW2 = 0;
                        for (int i = dataSeries.Count - 1; i >= 0; i--)
                        {
                            if (!dataSeries[i].Visible) { continue; }
                            //
                            SizeF sz = this.LabelSizes[i];
                            float tw = sz.Width + LegendPadding + lgnIconSize;
                            lgnW  -= tw; // negate space required for first row
                            lgnW2 += tw; // and add it to second row
                            if (lgnW2 >= lgnW)
                            {   // once 2nd row exceeds 1st -- pick better fit, save, and break
                                if (lgnW2 - lgnW < lgnW - lgnW2 + tw * 2)
                                {   // current state is a better fit
                                    lblBreak = i - 1;
                                    this.Row1W = lgnW;
                                    this.Row2W = lgnW2;
                                    lgnW = lgnW2;
                                }
                                else
                                {   // previous state is a better fit
                                    lblBreak = i;
                                    lgnW += tw;
                                    this.Row1W = lgnW;
                                    this.Row2W = lgnW2 - tw;
                                }
                                break;
                            }
                        }
                        // no need to recalc the heigth of the legend -- simply double it and pad with 4px between rows
                        lgnH = lgnH * 2 + LegendPadding;
                    }

                    // add 2px padding on each edge
                    lgnH += LegendPadding;
                    // compute legend position
                    lgnX = x + (w - lgnW) / 2F; // center legend
                    lgnY = (this.DockTop) ? y + LegendPadding : y + h - lgnH - LegendPadding;
                }

                // update size of series icon and row break
                this.IconSize = lgnIconSize;
                this.LabelBreakIndex = lblBreak;
                // update legend rectangle
                this.Bounds = new Rectangle((int)lgnX, (int)lgnY, (int)(lgnW + 0.5), (int)(lgnH + 0.5));
            }
            void UpdateLabelSizes()
            {
                List<DataSeries> dataSeries = this.Parent.DataSeries;
                Font             legendFont = this.Parent.ChartFontSm;
                //
                // init/update label sizes array
                if (this.LabelSizes == null || this.LabelSizes.Length != dataSeries.Count)
                {
                    this.LabelSizes = new SizeF[dataSeries.Count];
                }
                // measure ...
                using (Graphics g = this.Parent.CreateGraphics())
                {
                    for (int i = 0; i < dataSeries.Count; i++)
                    {
                        if (dataSeries[i].Visible)
                        {
                            this.LabelSizes[i] = g.MeasureString(dataSeries[i].Name, legendFont);
                        }
                    }
                }
            }
            /// <summary>
            /// Paints the legend on the chart.
            /// </summary>
            /// <param name="g">The graphics object to use for painting.</param>
            protected override void PaintInternal(Graphics g)
            {
                if (!this.Visible) { return; }
                //
                GridInfo  grid = this.Parent.Grid;
                var dataSeries = this.Parent.DataSeries;
                var legendFont = this.Parent.ChartFontSm;
                //
                using (Brush b = new SolidBrush(this.BackColor)) { g.FillRectangle(b, this.Bounds); }
                using (Pen   p = new Pen  (grid.GridLinesColor)) { g.DrawRectangle(p, this.Bounds); }
                //
                float x = this.Bounds.X + LegendPadding / 2;
                float y = this.Bounds.Y + LegendPadding / 2;
                // adjust if multiple rows
                if (this.LabelBreakIndex != -1)
                {
                    if (this.Row1W < this.Row2W) { x += (this.Row2W - this.Row1W) / 2; }
                }
                //
                for (int i = 0; i < dataSeries.Count; i++)
                {
                    if (dataSeries[i].Visible)
                    {
                        SizeF sz = this.LabelSizes[i];
                        int colorIndex = this.GetColorIndex(i);
                        //
                        using (Pen   borderPen = new Pen       (this.ColorPalette.GetBorderColor(colorIndex)))
                        using (Brush fillBrush = new SolidBrush(this.ColorPalette.GetFillColor  (colorIndex)),
                                     strBrush  = new SolidBrush(this.Parent.ForeColor))
                        {
                            g.FillRectangle(fillBrush, x, y + (sz.Height - this.IconSize) / 2, this.IconSize, this.IconSize);
                            g.DrawRectangle(borderPen, x, y + (sz.Height - this.IconSize) / 2, this.IconSize, this.IconSize);
                            g.DrawString(dataSeries[i].Name, legendFont, strBrush, x + this.IconSize, y);
                        }

                        if (this.DockLeft || this.DockRight)
                        {
                            y += Math.Max(this.IconSize, sz.Height) + LegendPadding;
                        }
                        else
                        {
                            x += sz.Width + LegendPadding + this.IconSize;
                        }
                    }

                    if ((this.DockTop || this.DockBottom) && this.LabelBreakIndex == i)
                    {   // start new row
                        x = this.Bounds.X + LegendPadding / 2 + ((this.Row2W < this.Row1W) ? (this.Row1W - this.Row2W) / 2 : 0);
                        y = this.Bounds.Y + this.Bounds.Height / 2 + LegendPadding / 2;
                    }

                }
            }

            #endregion

            #region /*** Properties ***/

            //----- provided properties -----
            internal bool Visible
            {
                get { return this._visible; }
                set
                {
                    if (this._visible != value)
                    {
                        this._visible = value;
                        this.SetDirty(true);
                        this.Parent.OnShowLegendChanged();
                    }
                }
            }
            internal LegendDockStyle Dock
            {
                get { return this._dock; }
                set
                {
                    if (this._dock != value)
                    {
                        this._dock = value;
                        this.SetDirty(true);
                        this.Parent.OnDockLegendChanged();
                    }
                }
            }
            internal Color BackColor
            {
                get
                {
                    if (this._backColor.IsEmpty)
                    {
                        if (this.Parent != null && this.Parent.Grid != null)
                        {
                            return ChartColors.PseudoInvertColor(this.Parent.Grid.BackColor);
                        }
                        return Control.DefaultBackColor;
                    }
                    else { return this._backColor; }
                }
                set
                {
                    if (this.BackColor != value)
                    {
                        this._backColor = value;
                        this.Invalidate();
                        this.Parent.OnLegendBackColorChanged();
                    }
                }
            }

            //----- read-only properties -----
            /// <summary>Gets whether the legend is docked at the top-center of the chart area.</summary>
            public bool DockTop { get { return this.Dock == LegendDockStyle.Top; } }
            /// <summary>Gets whether the legend is docked at the top-right of the chart area.</summary>
            public bool DockRight { get { return this.Dock == LegendDockStyle.Right; } }
            /// <summary>Gets whether the legend is docked at the bottom-center of the chart area.</summary>
            public bool DockBottom { get { return this.Dock == LegendDockStyle.Bottom; } }
            /// <summary>Gets whether the legend is docked at the top-left of the chart area.</summary>
            public bool DockLeft { get { return this.Dock == LegendDockStyle.Left; } }
            /// <summary>Gets the size of each series icon in the chart's legend.</summary>
            public float IconSize { get; private set; }
            /// <summary>Gets the sizes of each series label in the chart's legend.</summary>
            public SizeF[] LabelSizes { get; private set; }
            // -1 if single row legend; otherwise, index of label at which to start new row
            int LabelBreakIndex { get; set; }
            float Row1W { get; set; } // width of each row, if legend displayed on multiple rows
            float Row2W { get; set; }
            /// <summary>Gets the rectangle that defines the bounds of the chart's legend.</summary>
            public Rectangle Bounds { get; private set; }

            #endregion

            #region /*** Variables ***/

            bool            _visible   = true;
            LegendDockStyle _dock      = LegendDockStyle.Top;
            Color           _backColor = Color.Empty;

            #endregion

        }

        //----- ChartBase.LegendInfo instance -----
        /// <summary>Gets an object that handles layout and painting of the chart's legend.</summary>
        protected LegendInfo Legend { get; private set; }

        //----- ChartBase.LegendInfo Provided Properties -----
        #region < Show/Hide Legend Provided Properties >
        /// <summary>Gets or sets whether to display the legend on the chart control.</summary>
        [Browsable(true)]
        [DefaultValue(true)]
        [Category("Grid Legend")]
        [Description("Indicates whether to display the legend on the chart control.")]
        public bool ShowLegend { get { return this.Legend.Visible; } set { this.Legend.Visible = value; } }
        /// <summary>Gets or sets the legend's dock style within the chart control.</summary>
        [Browsable(true)]
        [DefaultValue(LegendDockStyle.Top)]
        [Category("Grid Legend")]
        [Description("The legend's dock style within the chart control.")]
        public LegendDockStyle DockLegend { get { return this.Legend.Dock; } set { this.Legend.Dock = value; } }
        /// <summary>Gets or set the background color of the chart's legend. Set to <see cref="Color.Empty"/> to automatically adjust
        ///   based on the grid's background color.</summary>
        [Browsable(true)]
        [DefaultValue(typeof(Color), "Empty")]
        [Category("Grid Legend")]
        [Description("The background color of the chart's legend. Set to Empty to automatically adjust based on the grid's background color.")]
        public Color LegendBackColor { get { return this.Legend.BackColor; } set { this.Legend.BackColor = value; } }
        #endregion
        #region < Show/Hide Legend Provided Property Events >
        /// <summary>Occurs when the chart's <see cref="ShowLegend"/> property changes.</summary>
        public event EventHandler ShowLegendChanged;
        /// <summary>Occurs when the chart's <see cref="DockLegend"/> property changes.</summary>
        public event EventHandler DockLegendChanged;
        /// <summary>Occurs when the chart's <see cref="LegendBackColor"/> property changes.</summary>
        public event EventHandler LegendBackColorChanged;
        #endregion
        #region < Show/Hide Legend methods for raising Provided Property Events >
        /// <summary>Raises the <see cref="ShowLegendChanged"/> event.</summary>
        protected virtual void OnShowLegendChanged() { this.ShowLegendChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="DockLegendChanged"/> event.</summary>
        protected virtual void OnDockLegendChanged() { this.DockLegendChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="LegendBackColorChanged"/> event.</summary>
        protected virtual void OnLegendBackColorChanged() { this.LegendBackColorChanged?.Invoke(this, EventArgs.Empty); }
        #endregion

        //----- ChartBase.LegendInfo Read-Only Properties -----
        #region < Show/Hide Legend Read-Only Properties >
        /// <summary>Gets the rectangle that defines the bounds of the chart's legend.</summary>
        [Browsable(false)]
        protected internal Rectangle LegendBounds { get { return this.Legend.Bounds; } }
        /// <summary>Gets the x-coordinate of the chart's legend.</summary>
        [Browsable(false)]
        protected internal int LegendX { get { return this.Legend.Bounds.X; } }
        /// <summary>Gets the y-coordinate of the chart's legend.</summary>
        [Browsable(false)]
        protected internal int LegendY { get { return this.Legend.Bounds.Y; } }
        /// <summary>Gets the width of the chart's legend.</summary>
        [Browsable(false)]
        protected internal int LegendWidth { get { return this.Legend.Bounds.Width; } }
        /// <summary>Gets the height of the chart's legend.</summary>
        [Browsable(false)]
        protected internal int LegendHeight { get { return this.Legend.Bounds.Height; } }
        #endregion

        #endregion

        #region /*** AxisInfo class and Provided Properties ***/

        //----- ChartBase.AxisInfo class -----
        /// <summary>
        /// Handles layout and painting of the chart's axis.
        /// </summary>
        [Serializable]
        protected class AxisInfo : BaseInfo
        {

            #region /*** Ctors ***/

            internal AxisInfo(Chart parent, AxisType axisType) : base(parent)
            {
                this.Type = axisType;
                this._visible = (axisType != AxisType.Secondary);
            }

            #endregion

            #region /*** Methods ***/

            #region /* Methods for raising events */

            void OnVisibleChanged()
            {
                if      (this.Type == AxisType.Primary  ) { this.Parent.OnShowPrimaryAxisChanged(); }
                else if (this.Type == AxisType.Secondary) { this.Parent.OnShowSecondaryAxisChanged(); }
                else
                {
                    throw new ArgumentException("AxisInfo.Visible property on a category axis may not be changed.");
                }
            }
            void OnShowLabelChanged()
            {
                if      (this.Type == AxisType.Category ) { this.Parent.OnShowCategoryAxisLabelChanged (); }
                else if (this.Type == AxisType.Primary  ) { this.Parent.OnShowPrimaryAxisLabelChanged  (); }
                else if (this.Type == AxisType.Secondary) { this.Parent.OnShowSecondaryAxisLabelChanged(); }
            }
            void OnLabelChanged()
            {
                if      (this.Type == AxisType.Category ) { this.Parent.OnCategoryAxisLabelChanged (); }
                else if (this.Type == AxisType.Primary  ) { this.Parent.OnPrimaryAxisLabelChanged  (); }
                else if (this.Type == AxisType.Secondary) { this.Parent.OnSecondaryAxisLabelChanged(); }
            }
            void OnTickMarksChanged()
            {
                if      (this.Type == AxisType.Category ) { this.Parent.OnCategoryAxisTickMarksChanged (); }
                else if (this.Type == AxisType.Primary  ) { this.Parent.OnPrimaryAxisTickMarksChanged  (); }
                else if (this.Type == AxisType.Secondary) { this.Parent.OnSecondaryAxisTickMarksChanged(); }
            }
            void OnMinValueChanged()
            {
                if      (this.Type == AxisType.Category ) { this.Parent.OnCategoryAxisMinValueChanged (); }
                else if (this.Type == AxisType.Primary  ) { this.Parent.OnPrimaryAxisMinValueChanged  (); }
                else if (this.Type == AxisType.Secondary) { this.Parent.OnSecondaryAxisMinValueChanged(); }
            }
            void OnMaxValueChanged()
            {
                if      (this.Type == AxisType.Category ) { this.Parent.OnCategoryAxisMaxValueChanged (); }
                else if (this.Type == AxisType.Primary  ) { this.Parent.OnPrimaryAxisMaxValueChanged  (); }
                else if (this.Type == AxisType.Secondary) { this.Parent.OnSecondaryAxisMaxValueChanged(); }
            }
            void OnNumberFormatChanged()
            {
                if      (this.Type == AxisType.Category ) { this.Parent.OnCategoryAxisNumberFormatChanged (); }
                else if (this.Type == AxisType.Primary  ) { this.Parent.OnPrimaryAxisNumberFormatChanged  (); }
                else if (this.Type == AxisType.Secondary) { this.Parent.OnSecondaryAxisNumberFormatChanged(); }
            }
            void OnDiscreteXValuesChanged()
            {
                if (this.Type == AxisType.Category) { this.Parent.OnCategoryAxisDiscreteValuesChanged(); }
                else
                {
                    throw new ArgumentException("AxisInfo.DiscreteXValues property on a primary/secondary axis may not be changed.");
                }
            }

            #endregion

            /// <summary>
            /// Returns the string representation of this <see cref="AxisInfo"/> instance.
            /// </summary>
            /// <returns>The string representation of the <see cref="AxisInfo"/> instance.</returns>
            public override string ToString()
            {
                return "{Axis Type=" + this.Type.ToString() + ", Visible=" + this.Visible + "}";
            }

            // helper methods
            AxisDockStyle InvertDock()
            {
                switch (this.Dock)
                {
                    case AxisDockStyle.Bottom: return AxisDockStyle.Top;
                    case AxisDockStyle.Top   : return AxisDockStyle.Bottom;
                    case AxisDockStyle.Left  : return AxisDockStyle.Right;
                    case AxisDockStyle.Right : return AxisDockStyle.Left;
                }
                return AxisDockStyle.Top;
            }
            string FormatNumber(double value)
            {
                if (this.NumberFormat == null || this.NumberFormat == string.Empty)
                {   // use default format with 2 sig-digits
                    return value.ToString("G2");
                }
                return value.ToString(this.NumberFormat);
            }

            /// <summary>
            /// Updates the layout area of the chart's axis.
            /// </summary>
            protected override void UpdateLayoutInternal()
            {
                // axes layout operations are dependant upon one another
                // the category axis controls updates for all axes (since it is guaranteed to always be visible)
                // the layout updates for primary and secondary axes are, thus, redundant and will be skipped
                if (this.Type == AxisType.Category)
                {
                    this.UpdateGlobalLayout();
                }
            }
            void UpdateGlobalLayout()
            {
                //--------------------------------------------------------------------------------------------------------------\\
                // Before updating layout of any axis, we need to first calculate the bounding rectangle taking all visible
                // axis into account. We need to do this since since axes layout are dependant upon one another and we want X
                // and Y axis to intersect at a "0,0" point.
                //
                // While calculating bounds, certain attributes of the axis (for ex: Dock and Flow) will be updated as well.
                //
                // Once the finalized bounds are computed, the remainder of layout calculations for each axis can be performed.
                //--------------------------------------------------------------------------------------------------------------//

                // get grid/legend info
                GridInfo     grid = this.Parent.Grid;
                LegendInfo legend = this.Parent.Legend;
                if (grid  .IsDirty) { grid  .UpdateLayout(); }
                if (legend.IsDirty) { legend.UpdateLayout(); }
                // grid bounds
                int  x = grid.Bounds.X;
                int  y = grid.Bounds.Y;
                int  w = grid.Bounds.Width;
                int  h = grid.Bounds.Height;
                int gs = grid.GridSize;
                // legend bounds
                int lw = 0, lh = 0;
                if (legend.Visible)
                {
                    lw = legend.Bounds.Width  + LegendPadding;
                    lh = legend.Bounds.Height + LegendPadding;
                }
                //
                // calculate initial bounds based on grid and legend bounds
                float pad = 4; // leave 4px padding on each side
                float  x1 = x +     pad + (legend.DockLeft   ? lw : 0);
                float  x2 = x + w - pad - (legend.DockRight  ? lw : 0);
                float  y1 = y +     pad + (legend.DockTop    ? lh : 0);
                float  y2 = y + h - pad - (legend.DockBottom ? lh : 0);
                //
                RectangleF initBounds = new RectangleF(x1, y1, x2 - x1, y2 - y1);

                // get data series stats
                DataSeriesStatistics dsStats = this.Parent.GetDataSeriesStatistics();

                // perform certain calculations for each axis that may inform global bounds:
                //  - update flow and dock
                //  - update tick values:
                //      * min/max values (if applicable)
                //      * whether to paint discrete X vals (for category only)
                //      * calculate max size (width and height) of potential tick values that will be painted
                foreach (AxisInfo axis in this.Parent.ChartAxes)
                {   // if primary/secondary axis is not visible and does not have and data series that use it, skip it
                    // if the axis is not visible, but has data series that use, we still need to update certain things
                    // for it (such as min/max values) so that data series points can be painted correctly
                    if (!axis.Visible && !dsStats.HasAxisValues(axis.Type)) { continue; }

                    axis.UpdateFlowAndDock(grid);
                    axis.UpdateTickLabels(dsStats);
                    axis.MeasureLabels();

                    if (axis.Visible)
                    {   // adjust x/y to allow for tick marks (+2px), tick labels, and axis label to fit
                        // (only update and adjust for axis if it is visible)
                        // for fitting axis label:
                        //  - adjust only if docked top or bottom for now
                        //  - partiallly adjust if docked left or right; complete after all axes were processed
                        switch (axis.Dock)
                        {
                            case AxisDockStyle.Left  : x1 += (axis.TickLabelSize.Width  + 2); break;
                            case AxisDockStyle.Right : x2 -= (axis.TickLabelSize.Width  + 2); break;
                            case AxisDockStyle.Top   : y1 += (axis.TickLabelSize.Height + 2 + (axis.HasLabel ? axis.LabelSize.Height : 0)); break;
                            case AxisDockStyle.Bottom: y2 -= (axis.TickLabelSize.Height + 2 + (axis.HasLabel ? axis.LabelSize.Height : 0)); break;
                        }
                    }
                }
                // adjust x1 to allow for category axis label to fit if docked on left
                AxisInfo xAxis = this.Parent.CategoryAxis;
                if (xAxis.HasLabel && xAxis.Dock == AxisDockStyle.Left)
                {
                    x1 += xAxis.LabelSize.Width;
                }
                // adjust y1/y2 to allow for primary and secondary axis labels to fit if docked left/right
                // y-axis labels do not stack -- pick max height to adjust by
                AxisInfo y1Axis = this.Parent.PrimaryAxis;
                AxisInfo y2Axis = this.Parent.SecondaryAxis;
                float yLabelH = 0;
                if (y1Axis.Visible && y1Axis.HasLabel) { yLabelH =                   y1Axis.LabelSize.Height + y1Axis.TickLabelSize.Height / 2; }
                if (y2Axis.Visible && y2Axis.HasLabel) { yLabelH = Math.Max(yLabelH, y2Axis.LabelSize.Height + y2Axis.TickLabelSize.Height / 2); }
                if (xAxis.DockBottom) { y1 += yLabelH; } else if (xAxis.DockTop) { y2 -= yLabelH; }

                // align aixs bounds to have a minimum of grid-size padding at each edge
                x1 = Math.Max(x1, x + gs);
                x2 = Math.Min(x2, x + w - gs);
                y1 = Math.Max(y1, y + gs);
                y2 = Math.Min(y2, y + h - gs);

                // at this point, the global axes bounds have been established
                // (this may be updated if midpoint tick mark labels of axes docked at left or right are wider than min/max labels)
                RectangleF bounds = new RectangleF(x1, y1, x2 - x1, y2 - y1);

                // finalize layout calculations
                //  - update axis start/end points and length
                //  - update tick-marks (if required)
                //  - update axis label location
                //
                // since bounds calculations do not fully account for all tick labels (only min/max are computed), axis
                // that are docked at left or right should be positioned first, afterwards:
                //  1) compute tick label sizes for all mid point ticks
                //  2) adjust x1/x2 bounds if needed
                //  3) finalize layout for axis docked at top or bottom
                //
                foreach (AxisDockStyle dockStyle in AxisEvalOrder)
                {
                    AxisInfo axis = Array.Find(this.Parent.ChartAxes, item => item.Dock == dockStyle);
                    if (axis == null) { continue; }
                    // update bounds
                    axis.PaintAxis = axis.Visible;
                    axis.Bounds    = bounds;
                    // update layout
                    axis.UpdateAxisLocation(grid);
                    axis.UpdateTickMarks(dsStats);
                    // determine if x1/x2 bounds need to be updated
                    axis.UpdateBounds(ref bounds, initBounds);
                }
                //
                foreach (AxisInfo axis in this.Parent.ChartAxes)
                {   // finalize bounds and layout
                    axis.Bounds = bounds;
                    axis.UpdateLabelLocation(grid);
                }

                // determine if there is enough room to comfortably paint all visible axes
                if (bounds.Width < gs || bounds.Height < gs)
                {
                    foreach (AxisInfo axis in this.Parent.ChartAxes) { axis.PaintAxis = false; }
                }
            }
            // phase 1 -- establish axes bounds
            void UpdateFlowAndDock(GridInfo grid)
            {
                switch (this.Type)
                {
                    case AxisType.Primary:
                    case AxisType.Secondary:
                        this.FlowDirection = (grid.IsSwapped ||
                                              grid.IsSwappedAndFlipped) ? AxisDirection.LeftToRight :
                                             (grid.IsFlipped          ) ? AxisDirection.TopToBottom : AxisDirection.BottomToTop;
                        this.Dock          = (grid.IsSwapped          ) ? AxisDockStyle.Bottom      :
                                             (grid.IsSwappedAndFlipped) ? AxisDockStyle.Top         : AxisDockStyle.Left;
                        if (this.Type == AxisType.Secondary)
                        {
                            this.Dock = this.InvertDock();
                        }
                        break;

                    case AxisType.Category:
                        this.FlowDirection = (grid.IsSwapped          ) ? AxisDirection.BottomToTop :
                                             (grid.IsSwappedAndFlipped) ? AxisDirection.TopToBottom : AxisDirection.LeftToRight;
                        this.Dock          = (grid.IsFlipped          ) ? AxisDockStyle.Top         :
                                             (grid.IsSwapped ||
                                              grid.IsSwappedAndFlipped) ? AxisDockStyle.Left        : AxisDockStyle.Bottom;
                        break;

                    default:
                        throw new ArgumentOutOfRangeException("Axis Type", "The specified value is not a valid AxisType.");
                }
            }
            void UpdateTickLabels(DataSeriesStatistics dsStats)
            {
                //
                // for category axis only, override with discrete X values if:
                //  - there are no category values defined in the data series -AND-
                //  - the category axis has discrete X values defined
                //
                // otherwise, update min/max values (if not specified):
                //  - if data series has axis values defined, use min/max axis range (calculated by statistics object)
                //  - if there are no axis values (or discrete X for cat. axis), use default min/max range (from statistics object)
                //
                this.PaintDiscreteX = (this.Type == AxisType.Category && !dsStats.HasCategoryValues && this.DiscreteXValues != null);
                if (!this.PaintDiscreteX)
                {
                    double min = (double.IsNaN(this.MinValue)) ? dsStats.GetMinValue(this.Type) : this.MinValue;
                    double max = (double.IsNaN(this.MaxValue)) ? dsStats.GetMaxValue(this.Type) : this.MaxValue;
                    // ensure max > min
                    this.EnsureMinMax(min, ref max);
                    //
                    if (dsStats.HasAxisValues(this.Type))
                    {   // pad min/max values
                        double avg = Math.Abs((min + max) / 2 * 0.01); // scale avg of min & max by 1%
                        // scale min/max by 5% and subtract/add delta-avg
                        min = min - avg - Math.Abs(min * 0.05);
                        max = max + avg + Math.Abs(max * 0.05);
                        // smooth min/max values
                        min = this.SmoothValue(min, true );
                        max = this.SmoothValue(max, false);
                    }
                    this.MinValueActual =          (double.IsNaN(this.MinValue)) ? min : this.MinValue;
                    this.MaxValueActual = Math.Max((double.IsNaN(this.MaxValue)) ? max : this.MaxValue, this.MinValueActual);
                }
            }
            double SmoothValue(double value, bool min)
            {
                double abs = Math.Abs(value);
                int n = 0;
                if (abs < 1) { while (abs <  1) { n++; abs *= 10; } }
                else         { while (abs > 10) { n++; abs /= 10; } }
                double pow = Math.Pow(10, n);
                bool small = (-1 < value && value < 1);
                if (min)
                {
                    return (small) ? Math.Floor(value * pow) / pow : Math.Floor(value / pow) * pow;
                }
                else
                {
                    return (small) ? Math.Ceiling(value * pow) / pow : Math.Ceiling(value / pow) * pow;
                }
            }
            void MeasureLabels()
            {
                using (Graphics g = this.Parent.CreateGraphics())
                {
                    // measure max size of tick labels
                    if (this.PaintDiscreteX)
                    {
                        float w = 0, h = 0;
                        for (int i = 0; i < this.DiscreteXValues.Length; i++)
                        {
                            SizeF sz = g.MeasureString(this.DiscreteXValues[i], this.Parent.ChartFontSm);
                            w = Math.Max(w, sz.Width);
                            h = Math.Max(h, sz.Height);
                        }
                        this.TickLabelSize = new SizeF(w, h);
                    }
                    else
                    {
                        SizeF szMin = g.MeasureString(this.FormatNumber(this.MinValueActual), this.Parent.ChartFontSm);
                        SizeF szMax = g.MeasureString(this.FormatNumber(this.MaxValueActual), this.Parent.ChartFontSm);
                        //
                        this.TickLabelSize = new SizeF(Math.Max(szMin.Width, szMax.Width), Math.Max(szMin.Height, szMax.Height));
                    }
                    // measure label
                    this.PaintLabel = this.HasLabel;
                    if (this.PaintLabel)
                    {
                        bool isVertical = false;
                        SizeF sz = g.MeasureString(this.Label, this.Parent.ChartFontSm);
                        if (this.DockLeft && this.Type == AxisType.Category)
                        {   // for category axis, if axis is docked on the left, check if painting label vertical saves space
                            using (StringFormat sf = new StringFormat(StringFormatFlags.DirectionVertical))
                            {
                                SizeF szV = g.MeasureString(this.Label, this.Parent.ChartFontSm, PointF.Empty, sf);
                                if (szV.Width < sz.Width)
                                {
                                    sz = szV;
                                    isVertical = true;
                                }
                            }
                        }
                        //
                        this.LabelSize = sz;
                        this.IsLabelVertical = isVertical;
                    }
                }
            }
            // phase 2 -- update layout and finalize bounds
            void UpdateAxisLocation(GridInfo grid)
            {
                // assign axis start/end points based on axis type and grid orientation
                float     x1 = this.Bounds.Left;
                float     x2 = this.Bounds.Right;
                float     y1 = this.Bounds.Top;
                float     y2 = this.Bounds.Bottom;
                bool flipped = (grid.Orientation == GridOrientation.Flipped);
                bool swapped = (grid.Orientation == GridOrientation.Swapped);
                bool swAndFl = (grid.Orientation == GridOrientation.SwappedAndFlipped);
                //
                if (this.Type == AxisType.Category)
                {   // assign axis start/end points
                    this.StartLocation = new PointF(                            x1, (flipped || swAndFl) ? y1 : y2);
                    this.EndLocation   = new PointF((swapped || swAndFl) ? x1 : x2, (flipped || swapped) ? y1 : y2);
                    this.Length        = (swapped || swAndFl) ? y2 - y1 : x2 - x1;
                }
                else if (this.Type == AxisType.Primary)
                {
                    this.StartLocation = new PointF(                            x1, (flipped || swAndFl) ? y1 : y2);
                    this.EndLocation   = new PointF((swapped || swAndFl) ? x2 : x1, (flipped || swapped) ? y2 : y1);
                    this.Length        = (swapped || swAndFl) ? x2 - x1 : y2 - y1;
                }
                else if (this.Type == AxisType.Secondary)
                {
                    this.StartLocation = new PointF((swapped || swAndFl) ? x1 : x2, (flipped || swapped) ? y1 : y2);
                    this.EndLocation   = new PointF(                            x2, (flipped || swAndFl) ? y2 : y1);
                    this.Length        = (swapped || swAndFl) ? x2 - x1 : y2 - y1;
                }
            }
            void UpdateTickMarks(DataSeriesStatistics dsStats)
            {
                if (this.PaintDiscreteX)
                {   // if using discrete X values, override user-defined tick marks:
                    this.TickMarksActual = this.DiscreteXValues.Length;
                    //// if using values labels, override user-defined tick marks:
                    ////  - allow at least 32 pixels between tick marks
                    ////  - if there are more labels than max allowable ticks that can fit on an axis, spread ticks evenly into num vals
                    //int numVals  = this.ValuesLabels.Length;
                    //int maxTicks = (int)this.Length / 32;
                    //this.TickMarksActual = (maxTicks > numVals) ? numVals : (numVals + 1) / (numVals / maxTicks + 1);
                }
                else if (this.TickMarks == 0)
                {   // if tick marks are not specified, auto-detect tick marks:
                    //  - allow 128, 64, 32, or 16 pixels between ticks based on size
                    //  - switch to lower pixels if calc'ed ticks < 4
                    int maxTicks = (int)this.Length / 128 + 1;
                    if (maxTicks < MinTickMarks) { maxTicks = (int)this.Length / 64 + 1; }
                    if (maxTicks < MinTickMarks) { maxTicks = (int)this.Length / 32 + 1; }
                    if (maxTicks < MinTickMarks) { maxTicks = (int)this.Length / 16 + 1; }
                    this.TickMarksActual = Math.Max(2, maxTicks); // cap min ticks at 2
                }
                else
                {   // use user-defined tick marks
                    this.TickMarksActual = this.TickMarks;
                }
                // compute tick mark locations and labels along the axis
                this.TickMarkPos    = new float [this.TickMarksActual];
                this.TickMarkLabels = new string[this.TickMarksActual];
                this.TickMarkSizes  = new SizeF [this.TickMarksActual];
                this.TickWidth      = this.Length / (this.TickMarksActual + ((this.PaintDiscreteX) ? 0 : -1));
                float tickW         = this.TickWidth, tick0 = 0;
                if (this.PaintDiscreteX)
                {
                    tick0 = tickW / 2;
                    if (this.FlowBTT) { tick0 = -tick0; }
                }
                tick0 += (this.FlowLTR ? this.StartLocation.X : this.StartLocation.Y);
                if (this.FlowDirection == AxisDirection.BottomToTop) { tickW = -tickW; }
                //
                using (Graphics g = this.Parent.CreateGraphics())
                {
                    double ticksDelta = (this.MaxValueActual - this.MinValueActual) / (this.TickMarksActual - 1);
                    float lblW = this.TickLabelSize.Width, lblH = this.TickLabelSize.Height;
                    for (int i = 0; i < this.TickMarksActual; i++)
                    {
                        this.TickMarkPos   [i] = tick0 + tickW * i;
                        this.TickMarkLabels[i] = (this.PaintDiscreteX) ? this.DiscreteXValues[i] : this.FormatNumber(this.MinValueActual + ticksDelta * i);
                        this.TickMarkSizes [i] = g.MeasureString(this.TickMarkLabels[i], this.Parent.ChartFontSm);
                        //
                        lblW = Math.Max(lblW, this.TickMarkSizes[i].Width );
                        lblH = Math.Max(lblH, this.TickMarkSizes[i].Height);
                    }
                    if (!this.PaintDiscreteX)
                    {
                        this.TickLabelSize = new SizeF(lblW, lblH);
                    }
                }
            }
            void UpdateBounds(ref RectangleF bounds, RectangleF initBounds)
            {
                if (!this.Visible || !(this.DockLeft || this.DockRight)) { return; }
                //
                float dBounds = (this.DockLeft) ? bounds.Left - initBounds.Left : initBounds.Right - bounds.Right;
                float   tickW = this.TickLabelSize.Width;
                float      dW = tickW - dBounds;
                //
                if (dBounds < tickW)
                {   // adjust bounds
                    bounds.X     += (this.DockLeft ? dW : 0);
                    bounds.Width -= dW;
                    // adjust axis X-location
                    float newX = this.StartLocation.X + (this.DockLeft ? dW : -dW);
                    this.StartLocation = new PointF(newX, this.StartLocation.Y);
                    this.EndLocation   = new PointF(newX, this.EndLocation  .Y);
                }
            }
            // phase 3 -- finalize layout
            void UpdateLabelLocation(GridInfo grid)
            {
                if (!this.PaintLabel) { return; }
                //
                // if axis flows left-to-right (that is, docked at top or bottom), always center label along x-axis
                // otherwise --
                //  - for category axis (axis always docked at left):
                //      align x-coordinate to the left of axis and tick values
                //      center label along y-axis
                //  - for values axis (primary or secondary):
                //      align x-coordinate so that it does not overhang the grid
                //      paint label right above/below y-axis (depending on flow direction)
                //
                float gridX1 = grid.Bounds.Left;
                float gridX2 = grid.Bounds.Right;
                float     x1 = this.Bounds.Left;
                float     x2 = this.Bounds.Right;
                float     y1 = this.Bounds.Top;
                float     y2 = this.Bounds.Bottom;
                float      x = 0, y = 0;
                if (this.FlowLTR)
                {
                    x = (x1 + x2 - this.LabelSize.Width) / 2;
                    y = (this.DockTop) ? y1 - 2 - this.TickLabelSize.Height - this.LabelSize.Height : y2 + 2 + this.TickLabelSize.Height;
                }
                else if (this.Type == AxisType.Category)
                {
                    x = x1 - 2 - this.TickLabelSize.Width - this.LabelSize.Width;
                    y = (y1 + y2 - this.LabelSize.Height) / 2;
                }
                else
                {
                    x = (this.DockLeft) ? Math.Max(x1 - this.LabelSize.Width / 2, gridX1 + 4) :
                                          Math.Min(x2 - this.LabelSize.Width / 2, gridX2 - this.LabelSize.Width - 4);
                    y = (this.FlowBTT) ? y1 - this.TickLabelSize.Height / 2 - this.LabelSize.Height : y2 + this.TickLabelSize.Height / 2;
                }
                //
                this.LabelLocation = new PointF(x, y);
            }

            /// <summary>
            /// Paints the axis on the chart.
            /// </summary>
            /// <param name="g">The graphics object to use for painting.</param>
            protected override void PaintInternal(Graphics g)
            {
                if (!this.PaintAxis) { return; }

                Color foreColor = this.Parent.ForeColor;
                Color lightColor = ChartColors.PseudoInvertColor(foreColor);
                //
                using (Pen     pen = new Pen       (foreColor))
                using (Pen  invPen = new Pen       (lightColor))
                using (Brush brush = new SolidBrush(foreColor))
                {
                    GridInfo grid = this.Parent.Grid;
                    int  gridLeft = grid.Bounds.Left;
                    int gridRight = grid.Bounds.Right;
                    float  startX = this.StartLocation.X;
                    float  startY = this.StartLocation.Y;
                    float    endX = this.EndLocation  .X;
                    float    endY = this.EndLocation  .Y;

                    // paint axis line
                    g.DrawLine(pen, this.StartLocation, this.EndLocation);

                    // paint axis label
                    if (this.PaintLabel)
                    {
                        using (StringFormat format = new StringFormat())
                        {
                            GraphicsState gState = g.Save();
                            PointF lblLoc = this.LabelLocation;
                            if (this.IsLabelVertical && !(this.Parent.Animation.IsRotate || this.Parent.Animation.IsRotate3D))
                            {
                                float pW = this.Parent.Width;
                                float pH = this.Parent.Height;
                                lblLoc = new PointF(pW - lblLoc.X - this.LabelSize.Width, pH - lblLoc.Y - this.LabelSize.Height);
                                g.RotateTransform(180);
                                g.TranslateTransform(pW, pH, MatrixOrder.Append);
                            }
                            if (this.IsLabelVertical)
                            {
                                format.FormatFlags |= StringFormatFlags.DirectionVertical;
                            }
                            g.DrawString(this.Label, this.Parent.ChartFontSm, brush, lblLoc, format);
                            if (this.IsLabelVertical) { g.Restore(gState); }
                        }
                    }

                    // paint tick marks and values labels
                    float prevPos =  0; // last pos of value label
                    //
                    for (int i = 0; i < this.TickMarkPos.Length; i++)
                    {
                        float     pos = this.TickMarkPos   [i];
                        string lblStr = this.TickMarkLabels[i];
                        SizeF   lblSZ = this.TickMarkSizes [i];
                        //
                        float tickX1, tickY1, tickX2, tickY2;
                        float lblX, lblY;
                        //
                        if (this.FlowLTR)
                        {
                            tickX1 = pos; tickY1 = startY - 2;
                            tickX2 = pos; tickY2 = startY + 2;
                            lblX   = pos - lblSZ.Width / 2;
                            lblY   = (this.DockBottom) ? startY + 2 : startY - 2 - lblSZ.Height;
                        }
                        else
                        {
                            tickX1 = startX - 2; tickY1 = pos;
                            tickX2 = startX + 2; tickY2 = pos;
                            lblX   = (this.DockRight) ? startX + 2 : startX - 2 - lblSZ.Width;
                            lblY   = pos - lblSZ.Height / 2;
                        }
                        //
                        bool drawLbl = (prevPos == 0) || (this.FlowLTR && lblX >= prevPos) || (this.FlowTTB && lblY >= prevPos) || (this.FlowBTT && lblY <= prevPos);
                        if (drawLbl)
                        {
                            g.DrawString(lblStr, this.Parent.ChartFontSm, brush, lblX, lblY);
                            g.DrawLine(pen, tickX1, tickY1, tickX2, tickY2);
                            //
                            if      (this.FlowLTR) { prevPos = lblX + lblSZ.Width ; }
                            else if (this.FlowTTB) { prevPos = lblY + lblSZ.Height; }
                            else if (this.FlowBTT) { prevPos = lblY - lblSZ.Height; }
                        }
                        else
                        {
                            g.DrawLine(invPen, tickX1, tickY1, tickX2, tickY2);
                        }
                    } // next i (tick-mark)
                }
            }

            /// <summary>
            /// Sets the object's layout area as dirty, optionally forcing the parent control to immediately invalidate its client
            /// area and redraw itself.
            /// </summary>
            /// <param name="invalidateParent">true, to invalidate the parent control; false, otherwise.</param>
            new void SetDirty(bool invalidateParent)
            {
                if (this.Type != AxisType.Category)
                {   // whenever primary or secondary axis is required to be updated (is set dirty), we need to
                    // force category axis to update as well, since it handles layout calculations for all axis
                    this.Parent.CategoryAxis.SetDirty(false);
                }
                base.SetDirty(invalidateParent);
            }

            /// <summary>
            /// Translates the specified point index of a series into the coresponding location on the category axis.
            /// </summary>
            /// <param name="index">The index of a series point to translate.</param>
            /// <returns>
            ///     <see cref="float.NegativeInfinity"/>, if index is less than 0;
            ///     <see cref="float.PositiveInfinity"/>, if index is greater than or equal to the number of discrete category values;
            ///     otherwise, the translated location on the cateogry axis coresponding to index.
            /// </returns>
            /// <exception cref="InvalidOperationException">If TranslateValue(index) is called on an axis that is not the chart's
            ///   category axis.</exception>
            public float TranslateValue(int index)
            {
                // return value:
                //  - float.NegativeInfinity: value is < minValue
                //  - float.PositiveInfinity: value is > maxValue
                //  - float.NaN             : value not valid
                //  - [float value]         : location along axis

                // translating index may only be used on category axis
                if (this.Type != AxisType.Category)
                {
                    throw new InvalidOperationException("TranslateValue(index) function call is only valid for CategoryAxis.");
                }

                // if discrete values are defined, grab tick-marks pos at index
                // otherwise, treat inded as actual value and position along axis betweem min and max
                if (this.PaintDiscreteX)
                {
                    if (index < 0) { return float.NegativeInfinity; }
                    if (index >= this.TickMarkPos.Length) { return float.PositiveInfinity; }
                    return this.TickMarkPos[index];
                }
                else
                {
                    return this.TranslateValue((double)(index + 1));
                }
            }
            /// <summary>
            /// Translates the specified series value into the coresponding location on the axis.
            /// </summary>
            /// <param name="value">The value to translate.</param>
            /// <returns>
            ///     0, if axis' min and max values are equal;
            ///     <see cref="float.NegativeInfinity"/>, if value is less than axis' min value;
            ///     <see cref="float.PositiveInfinity"/>, if value is greater than axis' max value;
            ///     otherwise, the translated location on the axis coresponding to value.
            /// </returns>
            public float TranslateValue(double value)
            {
                double min = this.MinValueActual;
                double max = this.MaxValueActual;
                if (value < min) { return float.NegativeInfinity; }
                if (value > max) { return float.PositiveInfinity; }
                float pos0 = (this.TickMarkPos == null || this.TickMarkPos.Length == 0) ? 0 : this.TickMarkPos[0];
                float inv = (this.FlowBTT) ? -1 : 1;
                float d = (max == min) ? 0 : pos0 + inv * this.Length * (float)((value - min) / (max - min));
                return d;
            }

            void EnsureMinMax()
            {
                if (!double.IsNaN(this._minValue) && !double.IsNaN(this._maxValue))
                {
                    this.EnsureMinMax(this._minValue, ref this._maxValue);
                }
            }
            void EnsureMinMax(double min, ref double max)
            {
                // set up traps in case min and/or max values were calculated as infinite
                //  - if both are infinity -- default to 0 and 1
                //  - if only min is infinity -- set to: max/2 - 1 (if max is 0, this sets min to -1)
                //  - if only max is infinity -- set to: min*2 + 1 (if min is 0, this sets max to 1)
                if      (double.IsInfinity(min) && double.IsInfinity(max)) { min = 0; max = 1; }
                else if (double.IsInfinity(min)) { min = max / 2 - 1; }
                else if (double.IsInfinity(max)) { max = min * 2 + 1; }
                else if (max <= min)
                {   // ensure max > min
                    max = (min < 0) ? -min : (min == 0) ? 1 : min * 2;
                }
            }

            #endregion

            #region /*** Properties ***/

            //----- provided properties -----
            internal bool Visible
            {
                get { return this._visible; }
                set
                {
                    if (this._visible != value)
                    {
                        this._visible = value;
                        this.SetDirty(true);
                        this.OnVisibleChanged();
                    }
                }
            }
            internal bool ShowLabel
            {
                get { return this._showLabel; }
                set
                {
                    if (this._showLabel != value)
                    {
                        this._showLabel = value;
                        this.SetDirty(true);
                        this.OnShowLabelChanged();
                    }
                }
            }
            internal string Label
            {
                get { return this._label; }
                set
                {
                    if (this._label != value)
                    {
                        this._label = value;
                        this.SetDirty(true);
                        this.OnLabelChanged();
                    }
                }
            }
            internal int TickMarks
            {
                get { return this._tickMarks; }
                set
                {
                    if (value < 0)
                    {
                        throw new ArgumentOutOfRangeException("TickMarks", "value must be greater than or equal to 0.");
                    }
                    if (value != 0 && value < MinTickMarks) { value = MinTickMarks; }
                    if (this._tickMarks != value)
                    {
                        this._tickMarks = value;
                        this.SetDirty(true);
                        this.OnTickMarksChanged();
                    }
                }
            }
            internal double MinValue
            {
                get { return this._minValue; }
                set
                {
                    if (this._minValue != value)
                    {
                        this._minValue = value;
                        this.EnsureMinMax();
                        this.SetDirty(true);
                        this.OnMinValueChanged();
                    }
                }
            }
            internal double MaxValue
            {
                get { return this._maxValue; }
                set
                {
                    if (this._maxValue != value)
                    {
                        this._maxValue = value;
                        this.EnsureMinMax();
                        this.SetDirty(true);
                        this.OnMaxValueChanged();
                    }
                }
            }
            internal string NumberFormat
            {
                get { return this._numberFormat; }
                set
                {
                    if (this._numberFormat != value)
                    {
                        this._numberFormat = value;
                        this.SetDirty(true);
                        this.OnNumberFormatChanged();
                    }
                }
            }
            internal string[] DiscreteXValues
            {
                get { return this._discreteXValues; }
                set
                {
                    if (this._discreteXValues != value)
                    {
                        this._discreteXValues = value;
                        this.MinValueActual = 0;
                        this.MaxValueActual = 0;
                        if (this.Type == AxisType.Category)
                        {
                            this.SetDirty(true);
                        }
                        this.OnDiscreteXValuesChanged();
                    }
                }
            }

            //----- read-only properties -----
            // axis bounds only include where the edges of X, Y1, and Y2 axes may be drawn, excluding axis labels, tick marks, and tick labels
            RectangleF Bounds { get; set; }
            /// <summary>Gets the x-coordinate of the left edge of the bounding rectangle that encloses the X, Y1, and Y2 axes (excluding axis labels, tick marks, and tick labels).</summary>
            public float Left { get { return this.Bounds.Left; } }
            /// <summary>Gets the x-coordinate of the right edge of the bounding rectangle that encloses the X, Y1, and Y2 axes (excluding axis labels, tick marks, and tick labels).</summary>
            public float Right { get { return this.Bounds.Right; } }
            /// <summary>Gets the y-coordinate of the top edge of the bounding rectangle that encloses the X, Y1, and Y2 axes (excluding axis labels, tick marks, and tick labels).</summary>
            public float Top { get { return this.Bounds.Top; } }
            /// <summary>Gets the y-coordinate of the bottom edge of the bounding rectangle that encloses the X, Y1, and Y2 axes (excluding axis labels, tick marks, and tick labels).</summary>
            public float Bottom { get { return this.Bounds.Bottom; } }

            /// <summary>Gets the actual number of tick marks.</summary>
            /// <remarks>This value will be calculated if <see cref="TickMarks"/> is 0.</remarks>
            public int TickMarksActual { get; private set; }
            /// <summary>Gets the actual min-value for the axis.</summary>
            /// <remarks>This value will be calculated if <see cref="MinValue"/> is <see cref="float.NaN"/>.</remarks>
            public double MinValueActual { get; private set; }
            /// <summary>Gets the actual max-value for the axis.</summary>
            /// <remarks>This value will be calculated if <see cref="MaxValue"/> is <see cref="float.NaN"/>.</remarks>
            public double MaxValueActual { get; private set; }
            // measured max size of tick labels
            SizeF TickLabelSize { get; set; }

            /// <summary>Gets the type of the chart's axis.</summary>
            public AxisType Type { get; private set; }

            /// <summary>Gets the flow direction of the chart's axis.</summary>
            public AxisDirection FlowDirection { get; private set; }
            /// <summary>Gets whether the chart's axis flows from left-to-right.</summary>
            public bool FlowLTR { get { return this.FlowDirection == AxisDirection.LeftToRight; } }
            /// <summary>Gets whether the chart's axis flows from bottom-to-top.</summary>
            public bool FlowBTT { get { return this.FlowDirection == AxisDirection.BottomToTop; } }
            /// <summary>Gets whether the chart's axis flows from top-to-bottom.</summary>
            public bool FlowTTB { get { return this.FlowDirection == AxisDirection.TopToBottom; } }

            /// <summary>Gets the dock location of the chart's axis.</summary>
            public AxisDockStyle Dock { get; private set; }
            /// <summary>Gets whether the chart's axis is docked at the top edge of the grid.</summary>
            public bool DockTop { get { return this.Dock == AxisDockStyle.Top; } }
            /// <summary>Gets whether the chart's axis is docked at the right edge of the grid.</summary>
            public bool DockRight { get { return this.Dock == AxisDockStyle.Right; } }
            /// <summary>Gets whether the chart's axis is docked at the bottom edge of the grid.</summary>
            public bool DockBottom { get { return this.Dock == AxisDockStyle.Bottom; } }
            /// <summary>Gets whether the chart's axis is docked at the left edge of the grid.</summary>
            public bool DockLeft { get { return this.Dock == AxisDockStyle.Left; } }

            /// <summary>Gets the location where the chart's axis begins.</summary>
            public PointF StartLocation { get; private set; }
            /// <summary>Gets the location where the chart's axis ends.</summary>
            public PointF EndLocation { get; private set; }
            /// <summary>Gets the length of the chart's axis.</summary>
            public float Length { get; private set; }
            /// <summary>Gets the width of tick marks.</summary>
            public float TickWidth { get; private set; }
            /// <summary>Gets the tick mark positions along the axis.</summary>
            public float[] TickMarkPos { get; private set; }
            /// <summary>Gets the tick mark labels.</summary>
            public string[] TickMarkLabels { get; private set; }
            /// <summary>Gets the tick mark label sizes.</summary>
            SizeF[] TickMarkSizes { get; set; }

            /// <summary>Gets the location of the chart's axis label.</summary>
            public PointF LabelLocation { get; private set; }
            // measured size of axis label
            SizeF LabelSize { get; set; }
            // whether to paint label vertical (X-axis only)
            bool IsLabelVertical { get; set; }
            // whether label text is present
            bool HasLabel { get { return this.ShowLabel && this.Label != null && this.Label.Trim() != string.Empty; } }

            // additional properties to control axis painting
            /// <summary>Gets whether the axis is enabled for painting. If axis becomes too small to fit comfortably in the provided
            ///   grid area, this value will be set to false.</summary>
            public bool PaintAxis { get; private set; }
            bool PaintDiscreteX { get; set; }
            bool PaintLabel { get; set; }

            #endregion

            #region /*** Variables ***/

            // constants/readonly
            const int MinTickMarks = 4; // min tick marks (n/a when using values labels)
            static readonly AxisDockStyle[] AxisEvalOrder = { AxisDockStyle.Left,  // enforce specific order of evaluation
                                                              AxisDockStyle.Right, // for axis when updating some layout parts
                                                              AxisDockStyle.Bottom,
                                                              AxisDockStyle.Top };

            // axis properties
            bool     _visible         = true;
            bool     _showLabel       = true;
            string   _label           = string.Empty; // null or empty = hidden (do not print label)
            int      _tickMarks       = 0;            // 0 = auto-detect (based on chart width and GridSize)
            double   _minValue        = double.NaN;   // nan = auto-detect
            double   _maxValue        = double.NaN;   // nan = auto-detect
            string   _numberFormat    = string.Empty; // number format for numeric axis values only (n/a if using values labels)
            string[] _discreteXValues = null;         // discrete X values (applies to category axis only and if data series has no XValues)

            #endregion

        }

        //----- ChartBase.AxisInfo instance -----
        const int CategoryAxisIndex  = 0;
        const int PrimaryAxisIndex   = 1;
        const int SecondaryAxisIndex = 2;
        AxisInfo[] ChartAxes { get; set; }
        /// <summary>Gets an object that handles layout and painting of the chart's category axis.</summary>
        protected AxisInfo CategoryAxis { get { return this.ChartAxes[CategoryAxisIndex]; } }
        /// <summary>Gets an object that handles layout and painting of the chart's primary values axis.</summary>
        protected AxisInfo PrimaryAxis { get { return this.ChartAxes[PrimaryAxisIndex]; } }
        /// <summary>Gets an object that handles layout and painting of the chart's secondary values axis.</summary>
        protected AxisInfo SecondaryAxis { get { return this.ChartAxes[SecondaryAxisIndex]; } }

        //----- ChartBase.AxisInfo Provided Properties -----
        // CategoryAxis
        #region < Show/Hide CategoryAxis Provided Properties >
        /// <summary>Gets or set whether to display the label for the category axis.</summary>
        [Browsable(true)]
        [DefaultValue(true)]
        [Category("Grid Axis")]
        [Description("Whether to display the label for the category axis.")]
        public bool ShowCategoryAxisLabel { get { return this.CategoryAxis.ShowLabel; } set { this.CategoryAxis.ShowLabel = value; } }
        /// <summary>Gets or set the label to display for the category axis, or <see cref="string.Empty"/> to hide the axis label.</summary>
        [Browsable(true)]
        [DefaultValue("")]
        [Category("Grid Axis")]
        [Description("The label to display for the category axis. Set ShowCategoryAxisLabel to false or leave this field blank to hide the axis label.")]
        public string CategoryAxisLabel { get { return this.CategoryAxis.Label; } set { this.CategoryAxis.Label = value; } }
        /// <summary>Gets or set the number of tick marks to display along the category axis, or specify 0 to auto-detect.</summary>
        [Browsable(true)]
        [DefaultValue(0)]
        [Category("Grid Axis")]
        [Description("The number of tick marks to display along the category axis, or specify 0 to auto-detect.")]
        public int CategoryAxisTickMarks { get { return this.CategoryAxis.TickMarks; } set { this.CategoryAxis.TickMarks = value; } }
        /// <summary>Gets or set the minimum value for the category axis, or specify <see cref="double.NaN"/> to auto-detect.</summary>
        [Browsable(true)]
        [DefaultValue(double.NaN)]
        [Category("Grid Axis")]
        [Description("The minimum value for the category axis, or specify NaN to auto-detect.")]
        public double CategoryAxisMinValue { get { return this.CategoryAxis.MinValue; } set { this.CategoryAxis.MinValue = value; } }
        /// <summary>Gets or set the maximum value for the category axis, or specify <see cref="double.NaN"/> to auto-detect.</summary>
        [Browsable(true)]
        [DefaultValue(double.NaN)]
        [Category("Grid Axis")]
        [Description("The maximum value for the category axis, or specify NaN to auto-detect.")]
        public double CategoryAxisMaxValue { get { return this.CategoryAxis.MaxValue; } set { this.CategoryAxis.MaxValue = value; } }
        /// <summary>Gets or set the number format to use for the category axis tick labels.</summary>
        [Browsable(true)]
        [DefaultValue("")]
        [Category("Grid Axis")]
        [Description("The number format to use for the category axis tick labels.")]
        public string CategoryAxisNumberFormat { get { return this.CategoryAxis.NumberFormat; } set { this.CategoryAxis.NumberFormat = value; } }
        /// <summary>Gets or set the discrete values for the category axis.</summary>
        [Browsable(true)]
        [DefaultValue(null)]
        [Category("Grid Axis")]
        [Description("Specifies the discrete values for the category axis.")]
        public string[] CategoryAxisDiscreteValues { get { return this.CategoryAxis.DiscreteXValues; } set { this.CategoryAxis.DiscreteXValues = value; } }
        #endregion
        #region < Show/Hide CategoryAxis Provided Property Events >
        /// <summary>Occurs when the chart's <see cref="ShowCategoryAxisLabel"/> property changes.</summary>
        public event EventHandler ShowCategoryAxisLabelChanged;
        /// <summary>Occurs when the chart's <see cref="CategoryAxisLabel"/> property changes.</summary>
        public event EventHandler CategoryAxisLabelChanged;
        /// <summary>Occurs when the chart's <see cref="CategoryAxisTickMarks"/> property changes.</summary>
        public event EventHandler CategoryAxisTickMarksChanged;
        /// <summary>Occurs when the chart's <see cref="CategoryAxisMinValue"/> property changes.</summary>
        public event EventHandler CategoryAxisMinValueChanged;
        /// <summary>Occurs when the chart's <see cref="CategoryAxisMaxValue"/> property changes.</summary>
        public event EventHandler CategoryAxisMaxValueChanged;
        /// <summary>Occurs when the chart's <see cref="CategoryAxisNumberFormat"/> property changes.</summary>
        public event EventHandler CategoryAxisNumberFormatChanged;
        /// <summary>Occurs when the chart's <see cref="CategoryAxisDiscreteValues"/> property changes.</summary>
        public event EventHandler CategoryAxisDiscreteValuesChanged;
        #endregion
        #region < Show/Hide CategoryAxis methods for raising Provided Property Events >
        /// <summary>Raises the <see cref="ShowCategoryAxisLabelChanged"/> event.</summary>
        protected virtual void OnShowCategoryAxisLabelChanged() { this.ShowCategoryAxisLabelChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="CategoryAxisLabelChanged"/> event.</summary>
        protected virtual void OnCategoryAxisLabelChanged() { this.CategoryAxisLabelChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="CategoryAxisTickMarksChanged"/> event.</summary>
        protected virtual void OnCategoryAxisTickMarksChanged() { this.CategoryAxisTickMarksChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="CategoryAxisMinValueChanged"/> event.</summary>
        protected virtual void OnCategoryAxisMinValueChanged() { this.CategoryAxisMinValueChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="CategoryAxisMaxValueChanged"/> event.</summary>
        protected virtual void OnCategoryAxisMaxValueChanged() { this.CategoryAxisMaxValueChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="CategoryAxisNumberFormatChanged"/> event.</summary>
        protected virtual void OnCategoryAxisNumberFormatChanged() { this.CategoryAxisNumberFormatChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="CategoryAxisDiscreteValuesChanged"/> event.</summary>
        protected virtual void OnCategoryAxisDiscreteValuesChanged() { this.CategoryAxisDiscreteValuesChanged?.Invoke(this, EventArgs.Empty); }
        #endregion
        // PrimaryAxis
        #region < Show/Hide PrimaryAxis Provided Properties >
        /// <summary>Gets or set whether the primary values axis is visible on the chart.</summary>
        [Browsable(true)]
        [DefaultValue(true)]
        [Category("Grid Axis")]
        [Description("Indicates whether the primary values axis is visible on the chart.")]
        public bool ShowPrimaryAxis { get { return this.PrimaryAxis.Visible; } set { this.PrimaryAxis.Visible = value; } }
        /// <summary>Gets or set whether to display the label for the primary values axis.</summary>
        [Browsable(true)]
        [DefaultValue(true)]
        [Category("Grid Axis")]
        [Description("Whether to display the label for the primary values axis.")]
        public bool ShowPrimaryAxisLabel { get { return this.PrimaryAxis.ShowLabel; } set { this.PrimaryAxis.ShowLabel = value; } }
        /// <summary>Gets or set the label to display for the primary values axis, or <see cref="string.Empty"/> to hide the axis label.</summary>
        [Browsable(true)]
        [DefaultValue("")]
        [Category("Grid Axis")]
        [Description("The label to display for the primary values axis. Set ShowPrimaryAxisLabel to false or leave this field blank to hide the axis label.")]
        public string PrimaryAxisLabel { get { return this.PrimaryAxis.Label; } set { this.PrimaryAxis.Label = value; } }
        /// <summary>Gets or set the number of tick marks to display along the primary values axis, or specify 0 to auto-detect.</summary>
        [Browsable(true)]
        [DefaultValue(0)]
        [Category("Grid Axis")]
        [Description("The number of tick marks to display along the primary values axis, or specify 0 to auto-detect.")]
        public int PrimaryAxisTickMarks { get { return this.PrimaryAxis.TickMarks; } set { this.PrimaryAxis.TickMarks = value; } }
        /// <summary>Gets or set the minimum value for the primary values axis, or specify <see cref="double.NaN"/> to auto-detect.</summary>
        [Browsable(true)]
        [DefaultValue(double.NaN)]
        [Category("Grid Axis")]
        [Description("The minimum value for the primary values axis, or specify NaN to auto-detect.")]
        public double PrimaryAxisMinValue { get { return this.PrimaryAxis.MinValue; } set { this.PrimaryAxis.MinValue = value; } }
        /// <summary>Gets or set the maximum value for the primary values axis, or specify <see cref="double.NaN"/> to auto-detect.</summary>
        [Browsable(true)]
        [DefaultValue(double.NaN)]
        [Category("Grid Axis")]
        [Description("The maximum value for the primary values axis, or specify NaN to auto-detect.")]
        public double PrimaryAxisMaxValue { get { return this.PrimaryAxis.MaxValue; } set { this.PrimaryAxis.MaxValue = value; } }
        /// <summary>Gets or set the number format to use for the primary values axis tick labels.</summary>
        [Browsable(true)]
        [DefaultValue("")]
        [Category("Grid Axis")]
        [Description("The number format to use for the primary values axis tick labels.")]
        public string PrimaryAxisNumberFormat { get { return this.PrimaryAxis.NumberFormat; } set { this.PrimaryAxis.NumberFormat = value; } }
        #endregion
        #region < Show/Hide PrimaryAxis Provided Property Events >
        /// <summary>Occurs when the chart's <see cref="ShowPrimaryAxis"/> property changes.</summary>
        public event EventHandler ShowPrimaryAxisChanged;
        /// <summary>Occurs when the chart's <see cref="ShowPrimaryAxisLabel"/> property changes.</summary>
        public event EventHandler ShowPrimaryAxisLabelChanged;
        /// <summary>Occurs when the chart's <see cref="PrimaryAxisLabel"/> property changes.</summary>
        public event EventHandler PrimaryAxisLabelChanged;
        /// <summary>Occurs when the chart's <see cref="PrimaryAxisTickMarks"/> property changes.</summary>
        public event EventHandler PrimaryAxisTickMarksChanged;
        /// <summary>Occurs when the chart's <see cref="PrimaryAxisMinValue"/> property changes.</summary>
        public event EventHandler PrimaryAxisMinValueChanged;
        /// <summary>Occurs when the chart's <see cref="PrimaryAxisMaxValue"/> property changes.</summary>
        public event EventHandler PrimaryAxisMaxValueChanged;
        /// <summary>Occurs when the chart's <see cref="PrimaryAxisNumberFormat"/> property changes.</summary>
        public event EventHandler PrimaryAxisNumberFormatChanged;
        #endregion
        #region < Show/Hide PrimaryAxis methods for raising Provided Property Events >
        /// <summary>Raises the <see cref="ShowPrimaryAxisChanged"/> event.</summary>
        protected virtual void OnShowPrimaryAxisChanged() { this.ShowPrimaryAxisChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="ShowPrimaryAxisLabelChanged"/> event.</summary>
        protected virtual void OnShowPrimaryAxisLabelChanged() { this.ShowPrimaryAxisLabelChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="PrimaryAxisLabelChanged"/> event.</summary>
        protected virtual void OnPrimaryAxisLabelChanged() { this.PrimaryAxisLabelChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="PrimaryAxisTickMarksChanged"/> event.</summary>
        protected virtual void OnPrimaryAxisTickMarksChanged() { this.PrimaryAxisTickMarksChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="PrimaryAxisMinValueChanged"/> event.</summary>
        protected virtual void OnPrimaryAxisMinValueChanged() { this.PrimaryAxisMinValueChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="PrimaryAxisMaxValueChanged"/> event.</summary>
        protected virtual void OnPrimaryAxisMaxValueChanged() { this.PrimaryAxisMaxValueChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="PrimaryAxisNumberFormatChanged"/> event.</summary>
        protected virtual void OnPrimaryAxisNumberFormatChanged() { this.PrimaryAxisNumberFormatChanged?.Invoke(this, EventArgs.Empty); }
        #endregion
        // SecondaryAxis
        #region < Show/Hide SecondaryAxis Provided Properties >
        /// <summary>Gets or set whether the secondary values axis is visible on the chart.</summary>
        [Browsable(true)]
        [DefaultValue(false)]
        [Category("Grid Axis")]
        [Description("Indicates whether the secondary values axis is visible on the chart.")]
        public bool ShowSecondaryAxis { get { return this.SecondaryAxis.Visible; } set { this.SecondaryAxis.Visible = value; } }
        /// <summary>Gets or set whether to display the label for the secondary values axis.</summary>
        [Browsable(true)]
        [DefaultValue(true)]
        [Category("Grid Axis")]
        [Description("Whether to display the label for the secondary values axis.")]
        public bool ShowSecondaryAxisLabel { get { return this.SecondaryAxis.ShowLabel; } set { this.SecondaryAxis.ShowLabel = value; } }
        /// <summary>Gets or set the label to display for the secondary values axis, or <see cref="string.Empty"/> to hide the axis label.</summary>
        [Browsable(true)]
        [DefaultValue("")]
        [Category("Grid Axis")]
        [Description("The label to display for the secondary values axis. Set ShowSecondaryAxisLabel to false or leave this field blank to hide the axis label.")]
        public string SecondaryAxisLabel { get { return this.SecondaryAxis.Label; } set { this.SecondaryAxis.Label = value; } }
        /// <summary>Gets or set the number of tick marks to display along the secondary values axis, or specify 0 to auto-detect.</summary>
        [Browsable(true)]
        [DefaultValue(0)]
        [Category("Grid Axis")]
        [Description("The number of tick marks to display along the secondary values axis, or specify 0 to auto-detect.")]
        public int SecondaryAxisTickMarks { get { return this.SecondaryAxis.TickMarks; } set { this.SecondaryAxis.TickMarks = value; } }
        /// <summary>Gets or set the minimum value for the secondary values axis, or specify <see cref="double.NaN"/> to auto-detect.</summary>
        [Browsable(true)]
        [DefaultValue(double.NaN)]
        [Category("Grid Axis")]
        [Description("The minimum value for the secondary values axis, or specify NaN to auto-detect.")]
        public double SecondaryAxisMinValue { get { return this.SecondaryAxis.MinValue; } set { this.SecondaryAxis.MinValue = value; } }
        /// <summary>Gets or set the maximum value for the secondary values axis, or specify <see cref="double.NaN"/> to auto-detect.</summary>
        [Browsable(true)]
        [DefaultValue(double.NaN)]
        [Category("Grid Axis")]
        [Description("The maximum value for the secondary values axis, or specify NaN to auto-detect.")]
        public double SecondaryAxisMaxValue { get { return this.SecondaryAxis.MaxValue; } set { this.SecondaryAxis.MaxValue = value; } }
        /// <summary>Gets or set the number format to use for the secondary values axis tick labels.</summary>
        [Browsable(true)]
        [DefaultValue("")]
        [Category("Grid Axis")]
        [Description("The number format to use for the secondary values axis tick labels.")]
        public string SecondaryAxisNumberFormat { get { return this.SecondaryAxis.NumberFormat; } set { this.SecondaryAxis.NumberFormat = value; } }
        #endregion
        #region < Show/Hide SecondaryAxis Provided Property Events >
        /// <summary>Occurs when the chart's <see cref="ShowSecondaryAxis"/> property changes.</summary>
        public event EventHandler ShowSecondaryAxisChanged;
        /// <summary>Occurs when the chart's <see cref="ShowSecondaryAxisLabel"/> property changes.</summary>
        public event EventHandler ShowSecondaryAxisLabelChanged;
        /// <summary>Occurs when the chart's <see cref="SecondaryAxisLabel"/> property changes.</summary>
        public event EventHandler SecondaryAxisLabelChanged;
        /// <summary>Occurs when the chart's <see cref="SecondaryAxisTickMarks"/> property changes.</summary>
        public event EventHandler SecondaryAxisTickMarksChanged;
        /// <summary>Occurs when the chart's <see cref="SecondaryAxisMinValue"/> property changes.</summary>
        public event EventHandler SecondaryAxisMinValueChanged;
        /// <summary>Occurs when the chart's <see cref="SecondaryAxisMaxValue"/> property changes.</summary>
        public event EventHandler SecondaryAxisMaxValueChanged;
        /// <summary>Occurs when the chart's <see cref="SecondaryAxisNumberFormat"/> property changes.</summary>
        public event EventHandler SecondaryAxisNumberFormatChanged;
        #endregion
        #region < Show/Hide SecondaryAxis methods for raising Provided Property Events >
        /// <summary>Raises the <see cref="ShowSecondaryAxisChanged"/> event.</summary>
        protected virtual void OnShowSecondaryAxisChanged() { this.ShowSecondaryAxisChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="ShowSecondaryAxisLabelChanged"/> event.</summary>
        protected virtual void OnShowSecondaryAxisLabelChanged() { this.ShowSecondaryAxisLabelChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="SecondaryAxisLabelChanged"/> event.</summary>
        protected virtual void OnSecondaryAxisLabelChanged() { this.SecondaryAxisLabelChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="SecondaryAxisTickMarksChanged"/> event.</summary>
        protected virtual void OnSecondaryAxisTickMarksChanged() { this.SecondaryAxisTickMarksChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="SecondaryAxisMinValueChanged"/> event.</summary>
        protected virtual void OnSecondaryAxisMinValueChanged() { this.SecondaryAxisMinValueChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="SecondaryAxisMaxValueChanged"/> event.</summary>
        protected virtual void OnSecondaryAxisMaxValueChanged() { this.SecondaryAxisMaxValueChanged?.Invoke(this, EventArgs.Empty); }
        /// <summary>Raises the <see cref="SecondaryAxisNumberFormatChanged"/> event.</summary>
        protected virtual void OnSecondaryAxisNumberFormatChanged() { this.SecondaryAxisNumberFormatChanged?.Invoke(this, EventArgs.Empty); }
        #endregion

        //----- ChartBase.AxisInfo Read-Only Properties -----
        #region < Show/Hide CategoryAxis Read-Only Properties >
        /// <summary>Gets the flow direction of the chart's category axis.</summary>
        [Browsable(false)]
        protected internal AxisDirection CategoryAxisFlowDirection { get { return this.CategoryAxis.FlowDirection; } }
        /// <summary>Gets the dock location of the chart's category axis.</summary>
        [Browsable(false)]
        protected internal AxisDockStyle CategoryAxisDock { get { return this.CategoryAxis.Dock; } }
        /// <summary>Gets the location where the chart's category axis begins.</summary>
        [Browsable(false)]
        protected internal PointF CategoryAxisStartLocation { get { return this.CategoryAxis.StartLocation; } }
        /// <summary>Gets the location where the chart's category axis ends.</summary>
        [Browsable(false)]
        protected internal PointF CategoryAxisEndLocation { get { return this.CategoryAxis.EndLocation; } }
        /// <summary>Gets the length of the chart's category axis.</summary>
        [Browsable(false)]
        protected internal float CategoryAxisLength { get { return this.CategoryAxis.Length; } }
        #endregion
        #region < Show/Hide PrimaryAxis Read-Only Properties >
        /// <summary>Gets the flow direction of the chart's primary axis.</summary>
        [Browsable(false)]
        protected internal AxisDirection PrimaryAxisFlowDirection { get { return this.PrimaryAxis.FlowDirection; } }
        /// <summary>Gets the dock location of the chart's primary axis.</summary>
        [Browsable(false)]
        protected internal AxisDockStyle PrimaryAxisDock { get { return this.PrimaryAxis.Dock; } }
        /// <summary>Gets the location where the chart's primary axis begins.</summary>
        [Browsable(false)]
        protected internal PointF PrimaryAxisStartLocation { get { return this.PrimaryAxis.StartLocation; } }
        /// <summary>Gets the location where the chart's primary axis ends.</summary>
        [Browsable(false)]
        protected internal PointF PrimaryAxisEndLocation { get { return this.PrimaryAxis.EndLocation; } }
        /// <summary>Gets the length of the chart's primary axis.</summary>
        [Browsable(false)]
        protected internal float PrimaryAxisLength { get { return this.PrimaryAxis.Length; } }
        #endregion
        #region < Show/Hide SecondaryAxis Read-Only Properties >
        /// <summary>Gets the flow direction of the chart's secondary axis.</summary>
        [Browsable(false)]
        protected internal AxisDirection SecondaryAxisFlowDirection { get { return this.SecondaryAxis.FlowDirection; } }
        /// <summary>Gets the dock location of the chart's secondary axis.</summary>
        [Browsable(false)]
        protected internal AxisDockStyle SecondaryAxisDock { get { return this.SecondaryAxis.Dock; } }
        /// <summary>Gets the location where the chart's secondary axis begins.</summary>
        [Browsable(false)]
        protected internal PointF SecondaryAxisStartLocation { get { return this.SecondaryAxis.StartLocation; } }
        /// <summary>Gets the location where the chart's secondary axis ends.</summary>
        [Browsable(false)]
        protected internal PointF SecondaryAxisEndLocation { get { return this.SecondaryAxis.EndLocation; } }
        /// <summary>Gets the length of the chart's secondary axis.</summary>
        [Browsable(false)]
        protected internal float SecondaryAxisLength { get { return this.SecondaryAxis.Length; } }
        #endregion

        #endregion

        #region /*** SeriesInfo class and Provided Properties ***/

        //----- ChartBase.SeriesInfo class -----
        /// <summary>
        /// Handles layout and painting of the chart's data-series.
        /// </summary>
        [Serializable]
        protected class SeriesInfo : BaseInfo
        {

            #region /*** Ctors ***/

            internal SeriesInfo(Chart parent) : base(parent) { }

            #endregion

            #region /*** Methods ***/

            /// <summary>
            /// Updates the layout information for all data-series available on the chart.
            /// </summary>
            protected override void UpdateLayoutInternal()
            {
                if (this.Parent.CategoryAxis.IsDirty)
                {
                    this.Parent.CategoryAxis.UpdateLayout();
                }
                //
                List<DataSeries> series = this.Parent.DataSeries;
                this.DSStats     = this.Parent.GetDataSeriesStatistics();
                this.SeriesScale = this.Parent.AltScaleFactor / 3;
                this.HasValues   = new bool  [series.Count];
                this.DSPoints    = new PointF[series.Count][];
                this.DSYOffset   = new float [series.Count][];
                this.XZeroY1     = this.TranslatePoint(-1, 0, 0, false);
                this.XZeroY2     = this.TranslatePoint(-1, 0, 0, true );
                //
                for (int i = 0; i < series.Count; i++)
                {
                    if (series[i].Visible)
                    {
                        this.UpdateSeriesLayout(i);
                    }
                }
                // calculate y-offsets for stacked series
                this.UpdateYOffset(ChartStyle.StackedBar, AxisType.Primary);
                this.UpdateYOffset(ChartStyle.StackedBar, AxisType.Secondary);
            }
            /// <summary>
            /// Updates the layout information for the data-series at the specified index.
            /// </summary>
            /// <param name="seriesIndex">The index of the series to update.</param>
            protected virtual void UpdateSeriesLayout(int seriesIndex)
            {
                DataSeries series = this.Parent.DataSeries[seriesIndex];
                this.HasValues[seriesIndex] = (series.Values != null);
                if (!this.HasValues[seriesIndex]) { return; }
                this.DSPoints [seriesIndex] = new PointF[series.Values.Length];
                this.DSYOffset[seriesIndex] = new float [series.Values.Length];
                for (int i = 0; i < series.Values.Length; i++)
                {
                    PointF pt = this.TranslateSeriesPoint(series, i);
                    this.DSPoints[seriesIndex][i] = pt;
                }
            }
            /// <summary>
            /// Translates the point at the specified index, for the specified series, to the X-/Y-coordiante on the chart's grid.
            /// </summary>
            /// <param name="series">The series to update.</param>
            /// <param name="index">The index of the point to update.</param>
            /// <returns>The translated point for the specified point index.</returns>
            protected virtual PointF TranslateSeriesPoint(DataSeries series, int index)
            {
                // determine x and y values
                double x, y = series.Values[index];
                if (this.DSStats.HasCategoryValues)
                {   // if all series have category values, use the current series x-values
                    // otherwise, use first series x-values
                    x = (this.DSStats.CategoryValuesAllSeries) ? series.XValues[index] : this.Parent.DataSeries[0].XValues[index];
                }
                else
                {   // data series does not have x-values -- use index to determine discrete x-value
                    x = double.NaN;
                }
                //
                return this.TranslatePoint(index, x, y, (series.Axis == AxisType.Secondary));
            }
            /// <summary>
            /// Translates the x/y values of a series point at the specified index, to the X-/Y-coordinate on the chart's grid.
            /// </summary>
            /// <param name="index">The index of the point to update.</param>
            /// <param name="x">The x-value (or category value) for a series point at the specified index to translate. If the
            ///   category axis uses discrete values, specify <see cref="double.NaN"/> to translate the value using the series
            ///   index instead.</param>
            /// <param name="y">The y-value (or axis value) for a series point at the specified index to translate.</param>
            /// <param name="isSecondary">true, if translating the point onto the secondary values axis; false, if translating
            ///   the point onto the primary values axis.</param>
            /// <returns>The translated point for the specified x/y values.</returns>
            protected virtual PointF TranslatePoint(int index, double x, double y, bool isSecondary)
            {
                AxisInfo  xAxis = this.Parent.CategoryAxis;
                AxisInfo y1Axis = this.Parent.PrimaryAxis;
                AxisInfo y2Axis = this.Parent.SecondaryAxis;
                // translate x into category axis
                float xPos = (double.IsNaN(x) && index != -1) ? xAxis.TranslateValue(index) : xAxis.TranslateValue(x);
                // translate y into values axis
                float yPos = (isSecondary) ? y2Axis.TranslateValue(y) : y1Axis.TranslateValue(y);
                //
                GridInfo grid = this.Parent.Grid;
                return (grid.IsSwapped || grid.IsSwappedAndFlipped) ? new PointF(yPos, xPos) : new PointF(xPos, yPos);
            }
            /// <summary>
            /// Updates the Y-coordinate offsets for stacked data series.
            /// </summary>
            void UpdateYOffset(ChartStyle style, AxisType axis)
            {
                AxisInfo xAxis = this.Parent.CategoryAxis;
                PointF   xZero = (axis == AxisType.Secondary) ? this.XZeroY2 : this.XZeroY1;
                PointF  xStart = (this.IsPointValid(xZero)) ? xZero : xAxis.StartLocation;
                float    start = (xAxis.FlowLTR) ? xStart.Y : xStart.X;
                //
                for (int i = 0; i < this.DSStats.MaxValuesCount; i++)
                {
                    float neg = start, pos = start;
                    //
                    for (int j = 0; j < this.Parent.DataSeries.Count; j++)
                    {
                        DataSeries ds = this.Parent.DataSeries[j];
                        if (this.HasValues[j] && ds.Visible && ds.Style == style && ds.Axis == axis && i < ds.Values.Length)
                        {
                            PointF pt = this.DSPoints[j][i];
                            if (!this.IsPointValid(pt)) { continue; }
                            float offset = ((xAxis.FlowLTR) ? pt.Y : pt.X) - start;
                            //
                            if      (ds.Values[i] <  0) { this.DSYOffset[j][i] = neg - start; neg += offset; }
                            else if (ds.Values[i] >= 0) { this.DSYOffset[j][i] = pos - start; pos += offset; }
                        }
                    } // next j (series)
                } // next i (series value index)
            }

            /// <summary>
            /// Paints the series on the chart.
            /// </summary>
            /// <param name="g">The graphics object to use for painting.</param>
            protected override void PaintInternal(Graphics g)
            {
                if (!this.Parent.CategoryAxis.PaintAxis) { return; }

                // to prevent overlaps, if data series have multiple styles, paint styles in specific order
                this._barChartIndex = 0;
                this._barChartCount = this.DSStats.BarStyleCount +
                                     (this.DSStats.HasStackedBarStyleY1 ? 1 : 0) +
                                     (this.DSStats.HasStackedBarStyleY2 ? 1 : 0);
                //
                this.PaintSeriesByStyle(g, ChartStyle.Pie           , AxisType.Category );
                this.PaintSeriesByStyle(g, ChartStyle.StackedBar    , AxisType.Primary  );
                this.PaintSeriesByStyle(g, ChartStyle.Bar           , AxisType.Primary  );
                this.PaintSeriesByStyle(g, ChartStyle.Bar           , AxisType.Secondary);
                this.PaintSeriesByStyle(g, ChartStyle.StackedBar    , AxisType.Secondary);
                this.PaintSeriesByStyle(g, ChartStyle.Line          , AxisType.Category );
                this.PaintSeriesByStyle(g, ChartStyle.ScatterPlot   , AxisType.Category);
                this.PaintSeriesByStyle(g, ChartStyle.ScatterPlotAlt, AxisType.Category);
            }
            void PaintSeriesByStyle(Graphics g, ChartStyle style, AxisType axis)
            {
                ChartStyle chartStyle = this.Parent.ChartStyle;
                //
                for (int i = 0; i < this.Parent.DataSeries.Count; i++)
                {
                    DataSeries series = this.Parent.DataSeries[i];
                    if (!this.HasValues[i] || !series.Visible) { continue; }
                    //
                    ChartStyle seriesStyle = (series.Style == ChartStyle.Default) ? chartStyle : series.Style;
                    if (style == seriesStyle && (axis == series.Axis || axis == AxisType.Category))
                    {
                        switch (style)
                        {
                            case ChartStyle.Line          : this.PaintSeriesLine       (g, i); break;
                            case ChartStyle.Bar           : this.PaintSeriesBar        (g, i, axis, BarChartPaintStyle.StartAtXAxis); this._barChartIndex++; break;
                            case ChartStyle.StackedBar    : this.PaintSeriesBar        (g, i, axis, BarChartPaintStyle.AwayFromZero); break;
                            case ChartStyle.Pie           : this.PaintSeriesPie        (g, i); break;
                            case ChartStyle.ScatterPlot   :
                            case ChartStyle.ScatterPlotAlt: this.PaintSeriesScatterPlot(g, i, (style == ChartStyle.ScatterPlot)); break;
                        }
                    }
                } // next data-series

                // increment bar index counter for stacked bar charts after all were analyzed
                if ( style == ChartStyle.StackedBar &&
                    ((axis == AxisType  .Primary   && this.DSStats.HasStackedBarStyleY1) ||
                     (axis == AxisType  .Secondary && this.DSStats.HasStackedBarStyleY2)))
                {
                    this._barChartIndex++;
                }
            }
            void PaintSeriesScatterPlot(Graphics g, int seriesIndex, bool fillPoint)
            {
                bool altColors = this.Parent.DataSeries[seriesIndex].AlternateValueColors;
                int colorIndex = this.GetColorIndex(seriesIndex);
                //
                foreach (PointF pt in this.DSPoints[seriesIndex])
                {
                    if (this.IsPointValid(pt))
                    {
                        this.PaintPoint(g, colorIndex, pt, fillPoint);
                        if (altColors) { colorIndex++; }
                    }
                } // next point
            }
            void PaintPoint(Graphics g, int colorIndex, PointF pt, bool fillPoint)
            {
                float            radius = Math.Max(1, this.SeriesScale * 2);
                float          diameter = radius * 2;
                Color   seriesFillColor = this.ColorPalette.GetFillColor  (colorIndex);
                Color seriesBorderColor = this.ColorPalette.GetBorderColor(colorIndex);
                //
                if (diameter > 5)
                {
                    using (Pen borderPen = new Pen(seriesBorderColor))
                    {
                        if (fillPoint)
                        {
                            using (Brush fillBrush = new SolidBrush(seriesFillColor))
                            {
                                g.FillEllipse(fillBrush, pt.X - radius, pt.Y - radius, diameter, diameter);
                            }
                        }
                        g.DrawEllipse(borderPen, pt.X - radius, pt.Y - radius, diameter, diameter);
                    }
                }
                else
                {
                    using (Brush fillBrush = new SolidBrush(seriesBorderColor))
                    {
                        g.FillEllipse(fillBrush, pt.X - radius, pt.Y - radius, diameter, diameter);
                    }
                }
            }
            void PaintSeriesLine(Graphics g, int seriesIndex)
            {
                bool altColors = this.Parent.DataSeries[seriesIndex].AlternateValueColors;
                int colorIndex = this.GetColorIndex(seriesIndex);
                int  lineWidth = (int)Math.Max(1, this.SeriesScale);
                //
                using (GraphicsPath gp = new GraphicsPath())
                {
                    foreach (PointF pt in this.DSPoints[seriesIndex])
                    {
                        if (this.IsPointValid(pt)) { gp.AddLine(pt, pt); }
                    } // next point
                    if (gp.PointCount > 0)
                    {
                        using (Pen pen = new Pen(this.ColorPalette.GetBorderColor(colorIndex), lineWidth))
                        {
                            g.DrawPath(pen, gp);
                        }
                        foreach (PointF pt in this.DSPoints[seriesIndex])
                        {
                            if (this.IsPointValid(pt))
                            {
                                this.PaintPoint(g, colorIndex, pt, true);
                                if (altColors) { colorIndex++; }
                            }
                        }
                    }
                }
            }
            void PaintSeriesBar(Graphics g, int seriesIndex, AxisType axis, BarChartPaintStyle paintStyle)
            {
                AxisInfo xAxis = this.Parent.CategoryAxis;
                PointF   xZero = (axis == AxisType.Secondary) ? this.XZeroY2 : this.XZeroY1;
                PointF  xStart = (this.IsPointValid(xZero) &&
                                   paintStyle == BarChartPaintStyle.AwayFromZero) ? xZero : xAxis.StartLocation;
                PointF    xEnd = xAxis.EndLocation;
                float  barTtlW = xAxis.TickWidth - (this.SeriesScale * 3);
                float     barW = barTtlW / this._barChartCount;
                float  xOffset = ((xAxis.FlowBTT) ? this._barChartCount - 1 - this._barChartIndex : this._barChartIndex) * barW - barTtlW / 2;
                //
                bool altColors = this.Parent.DataSeries[seriesIndex].AlternateValueColors;
                int colorIndex = this.GetColorIndex(seriesIndex);
                //
                for (int i = 0; i < this.DSPoints[seriesIndex].Length; i++)
                {
                    PointF     pt = this.DSPoints [seriesIndex][i];
                    float yOffset = this.DSYOffset[seriesIndex][i];
                    //
                    if (this.IsPointValid(pt))
                    {
                        float x, y, w, h;
                        if (xAxis.FlowLTR)
                        {   // x-axis top or bottom; y-axis at edge
                            this.PaintSeriesBarHelper_CalcBar(pt.X, pt.Y, xAxis.Top, xAxis.Bottom, barW, xStart.Y, xOffset, yOffset, out x, out y, out w, out h);
                        }
                        else
                        {   // x/y-axes flipped
                            this.PaintSeriesBarHelper_CalcBar(pt.Y, pt.X, xAxis.Left, xAxis.Right, barW, xStart.X, xOffset, yOffset, out y, out x, out h, out w);
                        }
                        //
                        this.PaintSeriesBarHelper_PaintBar(g, colorIndex, x, y, w, h);
                        if (altColors) { colorIndex++; }
                    }
                } // next point
            }
            void PaintSeriesBarHelper_CalcBar(float ptX, float ptY, float top, float bottom, float barW, float xStart, float xOffset, float yOffset,
                out float x, out float y, out float w, out float h)
            {
                x = ptX + xOffset;
                w = barW;
                if (xStart <= ptY)
                {   // bar flows down/right
                    h = Math.Max(1, ptY - xStart);
                    y = xStart + yOffset;
                    //
                    if (y + h > bottom)
                    {
                        h = Math.Max(0, bottom - y);
                    }
                }
                else
                {   // bar flows up/left
                    h = xStart - ptY;
                    y = ptY + yOffset;
                    //
                    if (y < top)
                    {
                        h = Math.Max(0, h + y - top);
                        y = top;
                    }
                }
            }
            void PaintSeriesBarHelper_PaintBar(Graphics g, int colorIndex, float x, float y, float w, float h)
            {
                Color   seriesFillColor = this.ColorPalette.GetFillColor  (colorIndex);
                Color seriesBorderColor = this.ColorPalette.GetBorderColor(colorIndex);
                if (w > 2 && h > 2)
                {
                    using (Pen   borderPen = new Pen       (seriesBorderColor))
                    using (Brush fillBrush = new SolidBrush(seriesFillColor  ))
                    {
                        g.FillRectangle(fillBrush, x, y, w, h);
                        g.DrawRectangle(borderPen, x, y, w, h);
                    }
                }
                else if (w > 0 && h > 0)
                {
                    using (Pen   borderPen = new Pen       (seriesBorderColor))
                    using (Brush fillBrush = new SolidBrush(seriesBorderColor))
                    {
                        g.FillRectangle(fillBrush, x, y, w, h);
                        g.DrawRectangle(borderPen, x, y, w, h);
                    }
                }
            }
            void PaintSeriesPie(Graphics g, int seriesIndex)
            {
            }

            //----- helper methods -----
            bool IsPointValid(PointF pt)
            {
                return !(float.IsInfinity(pt.X) || float.IsInfinity(pt.Y) || float.IsNaN(pt.X) || float.IsNaN(pt.Y));
            }

            #endregion

            #region /*** Properties ***/

            DataSeriesStatistics DSStats { get; set; }
            float SeriesScale { get; set; }

            /// <summary>Gets whether a specific data series has any values.</summary>
            public bool[] HasValues { get; private set; }
            /// <summary>Gets the translated points for all data series within the chart.</summary>
            public PointF[][] DSPoints { get; private set; }
            /// <summary>Gets the y-axis offsets for the translated data series points for "stacked" series styles.</summary>
            public float[][] DSYOffset { get; private set; }
            //
            PointF XZeroY1 { get; set; }
            PointF XZeroY2 { get; set; }

            #endregion

            #region /*** Variables ***/

            // bar charts & stacked-bar charts share the same space -- need to keep track for positioning
            int _barChartIndex;
            int _barChartCount;

            #endregion

        }

        //----- ChartBase.SeriesInfo instance -----
        /// <summary>Gets or sets an object that handles layout and painting of the chart's data-series.</summary>
        protected SeriesInfo Series { get; set; }

        #endregion

        #region /*** AnimationEffects class and Provided Properties ***/

        /// <summary>
        /// Provides animation and painting support for the chart.
        /// </summary>
        [Serializable]
        protected class AnimationEffects
        {

            #region /*** Events ***/

            /// <summary>Occurs after the animation effect has completed.</summary>
            public event EventHandler AnimationCompleted;

            #endregion

            #region /*** Ctors ***/

            /// <summary>
            /// Initializes a new instance of the <see cref="AnimationEffects"/> class.
            /// </summary>
            internal AnimationEffects(Chart parent)
            {
                this.Parent = parent;
            }

            #endregion

            #region /*** Methods ***/

            //----- methods for raising events -----
            /// <summary>
            /// Raises the <see cref="AnimationCompleted"/> event.
            /// </summary>
            void OnAnimationCompleted()
            {
                this.AnimationCompleted?.Invoke(this.Parent, EventArgs.Empty);
            }

            //----- tracking arrow demo -----
            /// <summary>
            /// Starts painting the tracking demo, if it is not already running.
            /// </summary>
            public void PaintTrackingDemo()
            {
                //lock (this)
                //{
                //    if (!this._isTrackingDemo)
                //    {
                //        this._isTrackingDemo = true;
                //        Thread trackingDemoThread = new Thread(new ThreadStart(this.PaintTrackingDemoThread));
                //        trackingDemoThread.Name = "ChartTrackingDemoThread";
                //        trackingDemoThread.Start();
                //    }
                //}
            }
            /// <summary>
            /// Cancels the tracking demo, if it is running.
            /// </summary>
            public void CancelTrackingDemo()
            {
                this._isTrackingDemo = false;
            }
            void PaintTrackingDemoThread()
            {
                // wait for any existing animation effect to complete before painting demo
                while (this.Effect != AnimationEffect.None)
                {
                    Thread.Sleep(31);
                }
                //
                // arrow activation sequence: none, left, up, right, down, none, all, none, clear
                //
                float fadeLevel = this.Parent.FadeLevel;
                //this.Parent.FadeLevel *= 0.75F;
                this.IsPaintingTrackingDemo = true;
                for (int i = 0; i < 14; i++)
                {
                    this.TrackingDemoActivatedArrow = (i > 8) ? 0 : (i > 4) ? i - 4 : i;
                    this.TrackingDemoActivateAll    = (i == 10 || i == 12);
                    this.Parent.Invoke(new MethodInvoker(this.Parent.Refresh));
                    // wait about 1/8 of a sec before switching frames
                    Thread.Sleep(125);
                    if (!this._isTrackingDemo) { break; }
                    if (i == 9 || i == 13)
                    {   // wait another 1/8 of a sec for some "pause" frames
                        Thread.Sleep(125);
                    }
                    if (!this._isTrackingDemo) { break; }
                }
                this.IsPaintingTrackingDemo = false;
                this.DisableTrackingArrow();
                //this.Parent.FadeLevel = fadeLevel;
                this.Parent.Invoke(new MethodInvoker(this.Parent.Refresh));
            }

            //----- swipe effect -----
            /// <summary>
            /// Begins the swipe effect by adjusting the geometric transform by the specified delta amount.
            /// </summary>
            /// <param name="delta">The amount by which to "swipe" the chart's grid.</param>
            public void BeginSwipe(PointF delta)
            {
                this.Effect = AnimationEffect.Swipe;
                this.SwipeDelta = delta;
            }
            /// <summary>
            /// Swipes the chart's grid out of view.
            /// </summary>
            public void SwipeOut()
            {
                this.SwipeCommon(-1);
            }
            /// <summary>
            /// Swipes the chart's grid into view.
            /// </summary>
            public void SwipeIn()
            {
                this.SwipeCommon(1);
                this.Reset();
            }
            void SwipeCommon(int swipeIn)
            { 
                Rectangle gb = this.Parent.GridBounds;
                float     gw = gb.Width;
                float     gh = gb.Height;
                float swipeX = this.SwipeDelta.X;
                float swipeY = this.SwipeDelta.Y;
                float  xMult = (swipeX == 0) ? 0 : (swipeX < 0) ? swipeIn : -swipeIn;
                float  yMult = (swipeY == 0) ? 0 : (swipeY < 0) ? swipeIn : -swipeIn;
                float  stepX, stepY;
                //
                if (swipeIn == 1)
                {   // swipe in
                    stepX = (Math.Abs(swipeX)) / Frames * xMult;
                    stepY = (Math.Abs(swipeY)) / Frames * yMult;
                }
                else
                {   // swipe out
                    stepX = (gw - Math.Abs(swipeX)) / Frames * xMult;
                    stepY = (gh - Math.Abs(swipeY)) / Frames * yMult;
                }
                //
                this.RunAnimation(AnimationEffect.Swipe, swipeX, stepX, swipeY, stepY);
            }

            //----- fade effect -----
            /// <summary>
            /// Begins the fade effect by adjusting the chart grid's fade-level to the specified amount.
            /// </summary>
            /// <param name="fadeLevel">The amount by which to "fade" the chart's grid.</param>
            public void BeginFade(float fadeLevel)
            {
                this.Effect = AnimationEffect.Fade;
                this.FadeLevel = fadeLevel;
            }
            /// <summary>
            /// Fades the chart's grid out of view.
            /// </summary>
            public void FadeOut()
            {
                this.FadeCommon(0);
            }
            /// <summary>
            /// Fades the chart's grid into view.
            /// </summary>
            public void FadeIn()
            {
                this.FadeCommon(1);
                this.Reset();
            }
            void FadeCommon(int fadeIn)
            {
                float initialFadeLevel = (this.IsFade) ? this.FadeLevel : this.Parent.FadeLevel;
                float step = (fadeIn - initialFadeLevel) / (Frames - 1);
                this.RunAnimation(AnimationEffect.Fade, initialFadeLevel, step, 0, 0);
            }

            //----- rotate effect -----
            /// <summary>
            /// Begins the rotate effect by adjusting the geometric transform by the specified angle.
            /// </summary>
            /// <param name="angle">The amount by which to rotate the chart's grid.</param>
            public void BeginRotate(int angle)
            {
                this.Effect = AnimationEffect.Rotate;
                this.RotationAngle = angle;
            }
            /// <summary>
            /// Rotates the chart's grid to the nearest 90 degree angle (either clockwise or counterclockwise).
            /// </summary>
            public void RotateNearest()
            {
                // rotate to nearest angle (90/180/270) and update grid orientation and legend dock
                int startingAngle = (this.RotationAngle < 0 ? 360 : 0) + this.RotationAngle;
                int rotationTicks = ((int)Math.Round(startingAngle / 90D)) % 5;
                //
                this.RotateCommon(startingAngle, rotationTicks);
            }
            /// <summary>
            /// Rotates the chart's grid by the specified angle around the grid's center. The angle is rounded up to 90 degree intervals.
            /// </summary>
            /// <param name="angle">The amount by which to rotate the chart's grid.</param>
            public void Rotate90(int angle)
            {
                int startingAngle = (angle < 0 ? 360 : 0) + angle;
                int rotationTicks = ((int)Math.Round(startingAngle / 90D)) % 5;
                //
                if (rotationTicks > 0)
                {
                    float fadeLevel = this.Parent.FadeLevel;
                    this.Parent.FadeLevel = 0.5F;
                    this.RotateCommon((angle < 0) ? 360 : 0, rotationTicks);
                    this.Parent.FadeLevel = fadeLevel;
                    this.Reset();
                }
            }
            void RotateCommon(int startingAngle, int rotationTicks)
            {
                int    nearestAngle = rotationTicks * 90;
                var gridOrientation = this.Parent.GridOrientation;
                var      legendDock = this.Parent.DockLegend;
                for (int i = 0; i < rotationTicks; i++)
                {
                    gridOrientation = NextGridOrientation[(int)gridOrientation];
                    legendDock      = NextLegendDock     [(int)legendDock     ];
                }
                // animate transition
                float step = (nearestAngle - startingAngle) / (float)Frames;
                this.RunAnimation(AnimationEffect.Rotate, startingAngle, step, 0, 0);
                //
                this.Parent.GridOrientation = gridOrientation;
                this.Parent.DockLegend      = legendDock;
                this.Reset();
            }

            //----- rotate Z-axis effect -----
            /// <summary>
            /// Begins the rotate3D effect by adjusting the geometric transform by the specified angle and around the specified axis.
            /// </summary>
            /// <param name="angle">The amount by which to rotate the chart's grid.</param>
            /// <param name="axis">The axis around which to rotate the chart's grid.</param>
            public void BeginRotate3D(int angle, RotationAxis axis)
            {
                this.Effect = AnimationEffect.Rotate3D;
                this.RotationAngle = angle;
                this.RotationAxis = axis;
            }
            /// <summary>
            /// Rotates the chart's grid to the nearest 180 degree angle (either clockwise or counterclockwise). Rotation is
            /// performed around the axis provided to the <see cref="BeginRotate3D"/> method.
            /// </summary>
            public void Rotate3DNearest()
            {
                // rotate to nearest 180 degree angle
                int startingAngle = (this.RotationAngle < 0 ? 360 : 0) + this.RotationAngle;
                int rotationTicks = ((int)Math.Round(startingAngle / 180D)) % 3;
                //
                this.Rotate3DCommon(startingAngle, rotationTicks);
            }
            /// <summary>
            /// Rotates the chart's grid 180 degrees around the grid's center and around the specified axis.
            /// </summary>
            /// <param name="axis">The axis around which to rotate the chart's grid.</param>
            public void Rotate3D180(RotationAxis axis)
            {
                //int startingAngle = (angle < 0 ? 360 : 0) + angle;
                //int rotationTicks = ((int)Math.Round(startingAngle / 180D)) % 3;
                ////
                //if (rotationTicks > 0)
                //{
                //    float fadeLevel = this.Parent.FadeLevel;
                //    this.Parent.FadeLevel = 0.5F;
                //    this.RotateCommon((angle < 0) ? 360 : 0, rotationTicks);
                //    this.Parent.FadeLevel = fadeLevel;
                //    this.Reset();
                //}
            }
            void Rotate3DCommon(int startingAngle, int rotationTicks)
            {
                int nearestAngle = rotationTicks * 180;
                float step = (nearestAngle - startingAngle) / (float)Frames;
                this.RunAnimation(AnimationEffect.Rotate3D, startingAngle, step, 0, 0);
                this.Reset();
            }

            //----- support methods -----
            void RunAnimation(AnimationEffect effect, float arg0, float arg1, float arg2, float arg3)
            {   //
                // This method executes the animation effect for supported animations.
                // The "default" total EffectTime, Timeout, and number of Frames to render are
                // defined as class-level constants.
                // However, due to different system performance levels and varying system loads,
                // we cannot guarantee the amount of time that the actual paint operation will
                // require. Thus, we employ a Stopwatch to measure the elapsed render time to
                // determine whether some frames should be dropped.
                //
                this.Effect = effect;
                Win32.TimeBeginPeriod(Timeout);
                Stopwatch sw = new Stopwatch();
                int elapsedTime = 0;
                while (elapsedTime < EffectTime)
                {
                    int i = elapsedTime / Timeout; // frame to render
                    switch (effect)
                    {
                        case AnimationEffect.Swipe:
                            // arg0 = swipeX
                            // arg1 = stepX
                            // arg2 = swipeY
                            // arg3 = stepY
                            this.SwipeDelta = new PointF(arg0 + arg1 * i, arg2 + arg3 * i);
                            break;
                        case AnimationEffect.Fade:
                            // arg0 = initialFadeLevel
                            // arg1 = step
                            this.FadeLevel = arg0 + arg1 * i;
                            break;
                        case AnimationEffect.Rotate:
                            // arg0 = angle
                            // arg1 = step
                            this.RotationAngle = (int)(arg0 + arg1 * (i + 1F));
                            break;
                        case AnimationEffect.Rotate3D:
                            // arg0 = angle
                            // arg1 = step
                            this.RotationAngle = (int)(arg0 + arg1 * (i + 1F));
                            break;
                    }
                    // render effect
                    sw.Reset();
                    sw.Start();
                    this.Parent.Refresh();
                    sw.Stop();
                    if (Timeout > sw.ElapsedMilliseconds)
                    {
                        sw.Start();
                        Thread.Sleep(Timeout - (int)sw.ElapsedMilliseconds);
                        sw.Stop();
                    }
                    elapsedTime += (int)sw.ElapsedMilliseconds;
                }
                Win32.TimeEndPeriod();
            }

            /// <summary>
            /// Resets animation effects.
            /// </summary>
            public void Reset()
            {
                this.Effect = AnimationEffect.None;
                this.SwipeDelta = Point.Empty;
                this.FadeLevel = 1F;
                this.RotationAngle = 0;
                this.TrackingArrowDirection = 0;
                this.TrackingArrowLocation = Point.Empty;
                this.ActivatedTrackingArrow = false;
                this.Parent.Refresh();
            }

            /// <summary>
            /// Enables the tracking arrow for painting.
            /// </summary>
            /// <param name="direction">The direction where the tracking arrow head points: 0 (do not paint arrow), 1 (left),
            ///   2 (up), 3 (right), 4 (down).</param>
            /// <param name="location">The midpoint location of the tracking arrow.</param>
            /// <param name="activated">true, to paint activated tracking arrow; false, to paint standard arrow.</param>
            public void EnableTrackingArrow(int direction, Point location, bool activated)
            {
                if (location.IsEmpty)
                {
                    Rectangle gridBounds = this.Parent.GridBounds;
                    location = new Point(gridBounds.X + gridBounds.Width / 2, gridBounds.Y + gridBounds.Height / 2);
                }
                //
                this.TrackingArrowDirection = direction;
                this.TrackingArrowLocation  = location;
                this.ActivatedTrackingArrow = activated;
            }
            /// <summary>
            /// Disables the tracking arrow from painting.
            /// </summary>
            public void DisableTrackingArrow()
            {
                this.TrackingArrowDirection = 0;
            }

            //----- paint support methods -----
            /// <summary>
            /// Paints the chart and all chart elements.
            /// </summary>
            /// <param name="g">The graphics object to use for painting.</param>
            public void Paint(Graphics g)
            {
                // to support animation effects, painting is done in stages:
                //  1) prepare transform
                //  2) paint chart elements affected by animation
                //  3) apply fading effects
                //  4) restore transfrom
                //  5) paint chart elements not affected by animation or transform movement
                GraphicsState gState = g.Save();
                this.PrepareTransform (g);
                this.PaintGridElements(g);
                this.ApplyFadeEffect  (g);

                // restore transform
                g.Restore(gState);
                // paint tracking arrow
                if (this.IsPaintingTrackingDemo)
                {
                    this.PaintTrackingDemo(g);
                }
                else
                {
                    this.PaintTrackingArrow(g);
                }
                // paint non animated elements
                this.PaintTitle(g);
            }
            /// <summary>
            /// Prepares the geometric transform associated with the specified graphics object for animation.
            /// </summary>
            /// <param name="g">The graphics object to update.</param>
            void PrepareTransform(Graphics g)
            {
                Rectangle bounds = this.Parent.GridBounds;
                PointF mid = new PointF(bounds.X + bounds.Width / 2F, bounds.Y + bounds.Height / 2F); ;
                Matrix transform;
                //
                switch (this.Effect)
                {
                    case AnimationEffect.Swipe:
                        PointF deltaT = this.SwipeDelta;
                        g.TranslateTransform(deltaT.X, deltaT.Y);
                        break;
                    //case AnimationEffect.Fade:
                    //    break;
                    case AnimationEffect.Rotate:
                        // calculate new transformation matrix for rotate effect
                        transform = new Matrix();
                        transform.Translate(mid.X, mid.Y);
                        transform.Rotate(this.RotationAngle);
                        transform.Translate(-mid.X, -mid.Y);
                        g.Transform = transform;
                        break;

                    case AnimationEffect.Rotate3D:
                        // calculate new transformation matrix for rotate-3D effect
                        int direction = (this.RotationAngle < 0) ? -1 : 1;
                        double  theta = this.RotationAngle / 180D * Math.PI;
                        float     cos = (float)Math.Cos(theta);
                        float     sin = (float)Math.Sin(theta);
                        bool  rotateX = (this.RotationAxis == RotationAxis.X);
                        //
                        if (Math.Round(cos, 10) == 0) { cos = 0.01F; }
                        if (Math.Round(sin, 10) == 0) { sin = 0.01F; }
                        //
                        transform = new Matrix(rotateX ? 1 : cos, 0, 0, rotateX ? cos : 1, mid.X, mid.Y);
                        transform.Translate(-mid.X, -mid.Y);
                        g.Transform = transform;
                        break;
                }
            }
            void PaintGridElements(Graphics g)
            {
                Chart parent = this.Parent;
                if (this.PaintGrid  ) { parent.Grid  .Paint(g); }
                if (this.PaintLegend) { parent.Legend.Paint(g); }
                if (this.PaintSeries) { parent.Series.Paint(g); }
                if (this.PaintAxes  ) { foreach (AxisInfo axis in parent.ChartAxes) { axis.Paint(g); } }
            }
            void ApplyFadeEffect(Graphics g)
            {
                Chart parent = this.Parent;
                Color  fadeColor = Color.Empty;
                //
                if (this.IsFade)
                {
                    fadeColor = ChartColors.FadeToGray(parent.BackColor, this.FadeLevel, true);
                }
                else if (this.Parent.FadeLevel != 1)
                {
                    fadeColor = ChartColors.FadeToBlack(parent.BackColor, parent.FadeLevel, true);
                }
                if (!fadeColor.IsEmpty)
                {
                    using (Brush brush = new SolidBrush(fadeColor))
                    {
                        g.FillRectangle(brush, this.Parent.GridBounds);
                    }
                }
            }
            void PaintTrackingArrow(Graphics g)
            {
                Chart         parent = this.Parent;
                Rectangle gridBounds = parent.GridBounds;
                //
                if (this.TrackingArrowDirection != 0)
                {
                    // calculate bounding rectangle (also serves as bounds of tracking circle)
                    float s = Math.Min(gridBounds.Height, gridBounds.Width) / 4F;
                    float x = Math.Max(gridBounds.X + 4F + s / 2F, Math.Min(this.TrackingArrowLocation.X, gridBounds.Right  - 4F - s / 2F));
                    float y = Math.Max(gridBounds.Y + 4F + s / 2F, Math.Min(this.TrackingArrowLocation.Y, gridBounds.Bottom - 4F - s / 2F));
                    RectangleF rect = new RectangleF(x - s / 2F, y - s / 2F, s, s);
                    // calculate path for nav arrow
                    PointF[] pts = ChartPaint.GetTrackingArrowBounds(x, y, s, this.TrackingArrowDirection);

                    // paint
                    //float fadeLevel = (this.IsFade) ? this.FadeLevel : parent.FadeLevel;
                    Color cDrk, cLgt;
                    if (this.IsPaintingTrackingDemo)
                    {
                        cDrk = parent.ColorPalette.GetBorderColor(DateTime.Now.Hour);
                        cLgt = parent.ColorPalette.GetFillColor  (DateTime.Now.Hour);
                        cDrk = ChartColors.FadeToBlack(cDrk, 0.75F, false);
                        cLgt = ChartColors.FadeToWhite(cLgt, 0.75F, false);
                    }
                    else
                    {
                        cDrk = ChartColors.FadeToBlack(parent.BackColor, 0.5F, true);
                        cLgt = ChartColors.FadeToWhite(parent.BackColor, 0.5F, true);
                    }
                    //
                    using (var brush = new SolidBrush(cLgt))
                    using (var   pen = new Pen       (cDrk))
                    using (var  path = new GraphicsPath())
                    {
                        if (!this.IsPaintingTrackingDemo)
                        {
                            g.FillEllipse(brush, rect);
                        }
                        // draw path and also fill based on whether chart-cycle should be activated
                        path.AddLines(pts);
                        if (this.ActivatedTrackingArrow)
                        {
                            g.FillPath(brush, path);
                        }
                        g.DrawPath(pen, path);
                    }
                }
            }
            void PaintTitle(Graphics g)
            {
                Chart parent = this.Parent;
                GridInfo grid = parent.Grid;
                if (parent.ShowTitle)
                {
                    using (Brush b = new SolidBrush(parent.ForeColor))
                    {
                        g.DrawString(parent._title, parent.TitleFont, b, parent.TitleLocation);
                    }
                    if (!grid.HasBorder)
                    {
                        using (Pen p = new Pen(parent.ForeColor))
                        {
                            int x = grid.Bounds.X;
                            int y = grid.Bounds.Y;
                            g.DrawLine(p, x, y, parent.Width - x, y);
                        }
                    }
                }
            }
            //
            void PaintTrackingDemo(Graphics g)
            {
                Rectangle gridBounds = this.Parent.GridBounds;
                int                x = gridBounds.X + gridBounds.Width  / 2;
                int                y = gridBounds.Y + gridBounds.Height / 2;
                int                s = Math.Min(gridBounds.Height, gridBounds.Width) / 8;
                //
                this.EnableTrackingArrow(1, new Point(x - s, y), (this.TrackingDemoActivateAll || this.TrackingDemoActivatedArrow == 1));
                this.PaintTrackingArrow(g);
                this.EnableTrackingArrow(2, new Point(x, y - s), (this.TrackingDemoActivateAll || this.TrackingDemoActivatedArrow == 2));
                this.PaintTrackingArrow(g);
                this.EnableTrackingArrow(3, new Point(x + s, y), (this.TrackingDemoActivateAll || this.TrackingDemoActivatedArrow == 3));
                this.PaintTrackingArrow(g);
                this.EnableTrackingArrow(4, new Point(x, y + s), (this.TrackingDemoActivateAll || this.TrackingDemoActivatedArrow == 4));
                this.PaintTrackingArrow(g);
            }

            // get/set painted grid elements
            bool GetPaintedGridElements(PaintedGridElements element)
            {
                return (this._gridElements & element) == element;
            }
            void SetPaintedGridElements(PaintedGridElements element, bool value)
            {
                bool oldValue = this.GetPaintedGridElements(element);
                if (oldValue != value)
                {
                    if (value) { this._gridElements |= ( element); }
                    else       { this._gridElements &= (~element); }
                    this.Parent.Invalidate();
                }
            }

            #endregion

            #region /*** Properties ***/

            //----- provided properties -----
            /// <summary>Gets or sets whether to paint the chart's grid on the chart.</summary>
            public bool PaintGrid   { get { return this.GetPaintedGridElements(PaintedGridElements.PaintGrid  ); } set { this.SetPaintedGridElements(PaintedGridElements.PaintGrid  , value); } }
            /// <summary>Gets or sets whether to paint the chart's legend on the chart.</summary>
            public bool PaintLegend { get { return this.GetPaintedGridElements(PaintedGridElements.PaintLegend); } set { this.SetPaintedGridElements(PaintedGridElements.PaintLegend, value); } }
            /// <summary>Gets or sets whether to paint the chart's axes on the chart.</summary>
            public bool PaintAxes   { get { return this.GetPaintedGridElements(PaintedGridElements.PaintAxes  ); } set { this.SetPaintedGridElements(PaintedGridElements.PaintAxes  , value); } }
            /// <summary>Gets or sets whether to paint the data series on the chart.</summary>
            public bool PaintSeries { get { return this.GetPaintedGridElements(PaintedGridElements.PaintSeries); } set { this.SetPaintedGridElements(PaintedGridElements.PaintSeries, value); } }

            //----- animation properties -----
            PointF SwipeDelta { get; set; }
            float FadeLevel { get; set; }
            int RotationAngle { get; set; }
            RotationAxis RotationAxis { get; set; }
            //
            int TrackingArrowDirection { get; set; }
            Point TrackingArrowLocation { get; set; }
            bool ActivatedTrackingArrow { get; set; }
            //
            internal bool IsPaintingTrackingDemo { get; private set; }
            int TrackingDemoActivatedArrow { get; set; }
            bool TrackingDemoActivateAll { get; set; }

            //----- read-only properties -----
            /// <summary>Gets the animation effect being displayed on the control.</summary>
            public AnimationEffect Effect { get; private set; }
            /// <summary>Gets whether the animation effect being displayed on the control is set to <see cref="AnimationEffect.Swipe"/>.</summary>
            public bool IsSwipe { get { return this.Effect == AnimationEffect.Swipe; } }
            /// <summary>Gets whether the animation effect being displayed on the control is set to <see cref="AnimationEffect.Fade"/>.</summary>
            public bool IsFade { get { return this.Effect == AnimationEffect.Fade; } }
            /// <summary>Gets whether the animation effect being displayed on the control is set to <see cref="AnimationEffect.Rotate"/>.</summary>
            public bool IsRotate { get { return this.Effect == AnimationEffect.Rotate; } }
            /// <summary>Gets whether the animation effect being displayed on the control is set to <see cref="AnimationEffect.Rotate3D"/>.</summary>
            public bool IsRotate3D { get { return this.Effect == AnimationEffect.Rotate3D; } }

            //----- other properties -----
            /// <summary>Gets the parent of this object.</summary>
            public Chart Parent { get; private set; }

            #endregion

            #region /*** Variables ***/

            // set up animation counters
            const int EffectTime = 200; // approximate number of milliseconds per animation effect
            const int Timeout = 8; // milliseconds to wait between drawing each frame
            const int Frames = EffectTime / Timeout; // number of frames to draw

            // rotation support
            static readonly GridOrientation[] NextGridOrientation = { GridOrientation.SwappedAndFlipped, GridOrientation.Swapped, GridOrientation.Default, GridOrientation.Flipped };
            static readonly LegendDockStyle[] NextLegendDock = { LegendDockStyle.Right, LegendDockStyle.Bottom, LegendDockStyle.Left, LegendDockStyle.Top };

            // flags for provided properties
            PaintedGridElements _gridElements = PaintedGridElements.All;

            // tracking demo support
            bool _isTrackingDemo = false;

            #endregion

        }

        //----- ChartBase.AnimationEffects instance -----
        /// <summary>Gets an object that provides animation and painting support for the chart.</summary>
        protected AnimationEffects Animation { get; private set; }

        #endregion

    }
}
