/// <summary>
        /// Paint FastPoint Chart.
        /// </summary>
        /// <param name="graph">The Chart Graphics object.</param>
        /// <param name="common">The Common elements object.</param>
        /// <param name="area">Chart area for this chart.</param>
        /// <param name="seriesToDraw">Chart series to draw.</param>
        virtual public void Paint(
            ChartGraphics graph,
            CommonElements common,
            ChartArea area,
            Series seriesToDraw)
        {
            Common = common;
            Graph  = graph;
            if (area.Area3DStyle.Enable3D)
            {
                // Initialize variables
                chartArea3DEnabled = true;
                matrix3D           = area.matrix3D;
            }
            else
            {
                chartArea3DEnabled = false;
            }

            //************************************************************
            //** Loop through all series
            //************************************************************
            foreach (Series series in common.DataManager.Series)
            {
                // Process non empty series of the area with FastPoint chart type
                if (String.Compare(series.ChartTypeName, Name, true, System.Globalization.CultureInfo.CurrentCulture) != 0 ||
                    series.ChartArea != area.Name ||
                    !series.IsVisible())
                {
                    continue;
                }

                // Get 3D series depth and Z position
                if (chartArea3DEnabled)
                {
                    area.GetSeriesZPositionAndDepth(series, out float seriesDepth, out seriesZCoordinate);
                    seriesZCoordinate += seriesDepth / 2.0f;
                }

                // Set active horizontal/vertical axis
                Axis   hAxis    = area.GetAxis(AxisName.X, series.XAxisType, (area.Area3DStyle.Enable3D) ? string.Empty : Series.XSubAxisName);
                Axis   vAxis    = area.GetAxis(AxisName.Y, series.YAxisType, (area.Area3DStyle.Enable3D) ? string.Empty : Series.YSubAxisName);
                double hAxisMin = hAxis.ViewMinimum;
                double hAxisMax = hAxis.ViewMaximum;
                double vAxisMin = vAxis.ViewMinimum;
                double vAxisMax = vAxis.ViewMaximum;

                // Get "PermittedPixelError" attribute.
                // By default use 1/3 of the marker size.
                float permittedPixelError = series.MarkerSize / 3f;
                if (series.IsCustomPropertySet(CustomPropertyName.PermittedPixelError))
                {
                    string attrValue = series[CustomPropertyName.PermittedPixelError];

                    bool parseSucceed = float.TryParse(attrValue, NumberStyles.Any, CultureInfo.CurrentCulture, out float pixelError);

                    if (parseSucceed)
                    {
                        permittedPixelError = pixelError;
                    }
                    else
                    {
                        throw (new InvalidOperationException(SR.ExceptionCustomAttributeValueInvalid2("PermittedPixelError")));
                    }

                    // "PermittedPixelError" attribute value should be in range from zero to 1
                    if (permittedPixelError < 0f || permittedPixelError > 1f)
                    {
                        throw (new InvalidOperationException(SR.ExceptionCustomAttributeIsNotInRange0to1("PermittedPixelError")));
                    }
                }

                // Get pixel size in axes coordinates
                SKSize pixelSize            = graph.GetRelativeSize(new SKSize(permittedPixelError, permittedPixelError));
                SKSize axesMin              = graph.GetRelativeSize(new SKSize((float)hAxisMin, (float)vAxisMin));
                double axesValuesPixelSizeX = Math.Abs(hAxis.PositionToValue(axesMin.Width + pixelSize.Width, false) - hAxis.PositionToValue(axesMin.Width, false));
                double axesValuesPixelSizeY = Math.Abs(vAxis.PositionToValue(axesMin.Height + pixelSize.Height, false) - vAxis.PositionToValue(axesMin.Height, false));

                // Create point marker brush
                SKPaint markerBrush      = new() { Color = (series.MarkerColor == SKColor.Empty) ? series.Color : series.MarkerColor, Style = SKPaintStyle.Fill };
                SKPaint emptyMarkerBrush = new() { Color = (series.EmptyPointStyle.MarkerColor == SKColor.Empty) ? series.EmptyPointStyle.Color : series.EmptyPointStyle.MarkerColor, Style = SKPaintStyle.Fill };

                // Create point marker border pen
                SKPaint borderPen      = null;
                SKPaint emptyBorderPen = null;
                if (series.MarkerBorderColor != SKColor.Empty && series.MarkerBorderWidth > 0)
                {
                    borderPen = new SKPaint()
                    {
                        Style = SKPaintStyle.Stroke, Color = series.MarkerBorderColor, StrokeWidth = series.MarkerBorderWidth
                    };
                }
                if (series.EmptyPointStyle.MarkerBorderColor != SKColor.Empty && series.EmptyPointStyle.MarkerBorderWidth > 0)
                {
                    emptyBorderPen = new SKPaint()
                    {
                        Style = SKPaintStyle.Stroke, Color = series.EmptyPointStyle.MarkerBorderColor, StrokeWidth = series.EmptyPointStyle.MarkerBorderWidth
                    };
                }

                // Check if series is indexed
                bool indexedSeries = ChartHelper.IndexedSeries(Common, series.Name);

                // Get marker size taking in consideration current DPIs
                int markerSize = series.MarkerSize;
                if (graph != null && graph.Graphics != null)
                {
                    // Marker size is in pixels and we do the mapping for higher DPIs
                    markerSize = Math.Max(markerSize, markerSize);
                }

                // Loop through all ponts in the series
                int         index            = 0;
                double      xValuePrev       = 0.0;
                double      yValuePrev       = 0.0;
                SKPoint     currentPoint     = SKPoint.Empty;
                double      xPixelConverter  = (graph.Common.ChartPicture.Width - 1.0) / 100.0;
                double      yPixelConverter  = (graph.Common.ChartPicture.Height - 1.0) / 100.0;
                MarkerStyle markerStyle      = series.MarkerStyle;
                MarkerStyle emptyMarkerStyle = series.EmptyPointStyle.MarkerStyle;
                foreach (DataPoint point in series.Points)
                {
                    // Get point X and Y values
                    double xValue = (indexedSeries) ? index + 1 : point.XValue;
                    xValue = hAxis.GetLogValue(xValue);
                    double yValue = vAxis.GetLogValue(point.YValues[0]);
                    bool   currentPointIsEmpty = point.IsEmpty;

                    // Check if point is completly out of the data scaleView
                    if (xValue < hAxisMin ||
                        xValue > hAxisMax ||
                        yValue < vAxisMin ||
                        yValue > vAxisMax)
                    {
                        xValuePrev = xValue;
                        yValuePrev = yValue;
                        ++index;
                        continue;
                    }

                    // Check if point may be skipped
                    if (index > 0 && Math.Abs(xValue - xValuePrev) < axesValuesPixelSizeX &&
                        Math.Abs(yValue - yValuePrev) < axesValuesPixelSizeY)
                    {
                        // Increase counter and proceed to the next data point
                        ++index;
                        continue;
                    }

                    // Get point pixel position
                    currentPoint.X = (float)
                                     (hAxis.GetLinearPosition(xValue) * xPixelConverter);
                    currentPoint.Y = (float)
                                     (vAxis.GetLinearPosition(yValue) * yPixelConverter);

                    // Draw point marker
                    MarkerStyle currentMarkerStyle = (currentPointIsEmpty) ? emptyMarkerStyle : markerStyle;
                    if (currentMarkerStyle != MarkerStyle.None)
                    {
                        this.DrawMarker(
                            graph,
                            point,
                            index,
                            currentPoint,
                            currentMarkerStyle,
                            markerSize,
                            (currentPointIsEmpty) ? emptyMarkerBrush : markerBrush,
                            (currentPointIsEmpty) ? emptyBorderPen : borderPen);
                    }

                    // Remember last point coordinates
                    xValuePrev = xValue;
                    yValuePrev = yValue;
                    ++index;
                }

                // Dispose used brushes and pens
                markerBrush.Dispose();
                emptyMarkerBrush.Dispose();
                if (borderPen != null)
                {
                    borderPen.Dispose();
                }
                if (emptyBorderPen != null)
                {
                    emptyBorderPen.Dispose();
                }
            }
        }
        /// <summary>
        /// Draws a 3D surface connecting the two specified points in 2D space.
        /// Used to draw Line based charts.
        /// </summary>
        /// <param name="area">Chart area reference.</param>
        /// <param name="graph">Chart graphics.</param>
        /// <param name="matrix">Coordinates transformation matrix.</param>
        /// <param name="lightStyle">LightStyle style (None, Simplistic, Realistic).</param>
        /// <param name="prevDataPointEx">Previous data point object.</param>
        /// <param name="positionZ">Z position of the back side of the 3D surface.</param>
        /// <param name="depth">Depth of the 3D surface.</param>
        /// <param name="points">Array of points.</param>
        /// <param name="pointIndex">Index of point to draw.</param>
        /// <param name="pointLoopIndex">Index of points loop.</param>
        /// <param name="tension">Line tension.</param>
        /// <param name="operationType">AxisName of operation Drawing, Calculating Path or Both</param>
        /// <param name="topDarkening">Darkenning scale for top surface. 0 - None.</param>
        /// <param name="bottomDarkening">Darkenning scale for bottom surface. 0 - None.</param>
        /// <param name="thirdPointPosition">Position where the third point is actually located or float.NaN if same as in "firstPoint".</param>
        /// <param name="fourthPointPosition">Position where the fourth point is actually located or float.NaN if same as in "secondPoint".</param>
        /// <param name="clippedSegment">Indicates that drawn segment is 3D clipped. Only top/bottom should be drawn.</param>
        /// <returns>Returns elemnt shape path if operationType parameter is set to CalcElementPath, otherwise Null.</returns>
        protected override SKPath Draw3DSurface(
            ChartArea area,
            ChartGraphics graph,
            Matrix3D matrix,
            LightStyle lightStyle,
            DataPoint3D prevDataPointEx,
            float positionZ,
            float depth,
            ArrayList points,
            int pointIndex,
            int pointLoopIndex,
            float tension,
            DrawingOperationTypes operationType,
            float topDarkening,
            float bottomDarkening,
            SKPoint thirdPointPosition,
            SKPoint fourthPointPosition,
            bool clippedSegment)
        {
            // Create graphics path for selection
            SKPath resultPath = ((operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath)
                ? new SKPath() : null;

            // Check if points are drawn from sides to center (do only once)
            if (centerPointIndex == int.MaxValue)
            {
                centerPointIndex = GetCenterPointIndex(points);
            }

            //************************************************************
            //** Find line first & second points
            //************************************************************
            DataPoint3D secondPoint     = (DataPoint3D)points[pointIndex];
            int         pointArrayIndex = pointIndex;
            DataPoint3D firstPoint      = ChartGraphics.FindPointByIndex(
                points,
                secondPoint.index - 1,
                (multiSeries) ? secondPoint : null,
                ref pointArrayIndex);

            // Fint point with line properties
            DataPoint3D pointAttr = secondPoint;

            if (prevDataPointEx.dataPoint.IsEmpty)
            {
                pointAttr = prevDataPointEx;
            }
            else if (firstPoint.index > secondPoint.index)
            {
                pointAttr = firstPoint;
            }

            // Adjust point visual properties
            SKColor        color     = (useBorderColor) ? pointAttr.dataPoint.BorderColor : pointAttr.dataPoint.Color;
            ChartDashStyle dashStyle = pointAttr.dataPoint.BorderDashStyle;

            if (pointAttr.dataPoint.IsEmpty && pointAttr.dataPoint.Color == SKColor.Empty)
            {
                color = SKColors.Gray;
            }
            if (pointAttr.dataPoint.IsEmpty && pointAttr.dataPoint.BorderDashStyle == ChartDashStyle.NotSet)
            {
                dashStyle = ChartDashStyle.Solid;
            }

            //************************************************************
            //** Create "middle" point
            //************************************************************
            DataPoint3D middlePoint = new();

            middlePoint.xPosition = secondPoint.xPosition;
            middlePoint.yPosition = firstPoint.yPosition;

            // Check if reversed drawing order required
            bool originalDrawOrder = true;

            if ((pointIndex + 1) < points.Count)
            {
                DataPoint3D p = (DataPoint3D)points[pointIndex + 1];
                if (p.index == firstPoint.index)
                {
                    originalDrawOrder = false;
                }
            }

            // Check in which order vertical & horizontal lines segments should be drawn
            if (centerPointIndex != int.MaxValue && pointIndex >= centerPointIndex)
            {
                originalDrawOrder = false;
            }

            // Draw two segments of the step line
            SKPath resultPathLine1, resultPathLine2;

            if (originalDrawOrder)
            {
                // Draw first line
                middlePoint.dataPoint = secondPoint.dataPoint;
                resultPathLine1       = graph.Draw3DSurface(
                    area, matrix, lightStyle, SurfaceNames.Top, positionZ, depth, color,
                    pointAttr.dataPoint.BorderColor, pointAttr.dataPoint.BorderWidth, dashStyle,
                    firstPoint, middlePoint,
                    points, pointIndex, 0f, operationType, LineSegmentType.First,
                    showPointLines, false,
                    area.ReverseSeriesOrder,
                    multiSeries, 0, true);

                // No second draw of the prev. front line required
                graph.frontLinePen = null;

                // Draw second line
                middlePoint.dataPoint = firstPoint.dataPoint;
                resultPathLine2       = graph.Draw3DSurface(
                    area, matrix, lightStyle, SurfaceNames.Top, positionZ, depth, color,
                    pointAttr.dataPoint.BorderColor, pointAttr.dataPoint.BorderWidth, dashStyle,
                    middlePoint, secondPoint,
                    points, pointIndex, 0f, operationType, LineSegmentType.Last,
                    showPointLines, false,
                    area.ReverseSeriesOrder,
                    multiSeries, 0, true);

                // No second draw of the prev. front line required
                graph.frontLinePen = null;
            }
            else
            {
                // Draw second line
                middlePoint.dataPoint = firstPoint.dataPoint;
                resultPathLine2       = graph.Draw3DSurface(
                    area, matrix, lightStyle, SurfaceNames.Top, positionZ, depth, color,
                    pointAttr.dataPoint.BorderColor, pointAttr.dataPoint.BorderWidth, dashStyle,
                    middlePoint, secondPoint,
                    points, pointIndex, 0f, operationType, LineSegmentType.Last,
                    showPointLines, false,
                    area.ReverseSeriesOrder,
                    multiSeries, 0, true);

                // No second draw of the prev. front line required
                graph.frontLinePen = null;

                // Draw first line
                middlePoint.dataPoint = secondPoint.dataPoint;
                resultPathLine1       = graph.Draw3DSurface(
                    area, matrix, lightStyle, SurfaceNames.Top, positionZ, depth, color,
                    pointAttr.dataPoint.BorderColor, pointAttr.dataPoint.BorderWidth, dashStyle,
                    firstPoint, middlePoint,
                    points, pointIndex, 0f, operationType, LineSegmentType.First,
                    showPointLines, false,
                    area.ReverseSeriesOrder,
                    multiSeries, 0, true);

                // No second draw of the prev. front line required
                graph.frontLinePen = null;
            }

            if (resultPath != null)
            {
                if (area.Common.ProcessModeRegions && resultPathLine1 != null && resultPathLine1.PointCount > 0)
                {
                    area.Common.HotRegionsList.AddHotRegion(
                        resultPathLine1,
                        false,
                        prevDataPointEx.dataPoint,
                        prevDataPointEx.dataPoint.series.Name,
                        prevDataPointEx.index - 1);
                }

                if (resultPathLine2 != null && resultPathLine2.PointCount > 0)
                {
                    resultPath.AddPath(resultPathLine2);
                }
            }
            return(resultPath);
        }