/// <summary> /// Paint FastLine 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; bool clipRegionSet = false; 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 FastLine 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 float permittedPixelError = 1.0f; 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)); // Create line pen SKPaint linePen = new() { Color = series.Color, StrokeWidth = series.BorderWidth }; linePen.PathEffect = ChartGraphics.GetPenStyle(series.BorderDashStyle, series.BorderWidth); linePen.StrokeCap = SKStrokeCap.Round; // Create empty line pen SKPaint emptyLinePen = new() { Style = SKPaintStyle.Stroke, Color = series.EmptyPointStyle.Color, StrokeWidth = series.EmptyPointStyle.BorderWidth }; emptyLinePen.PathEffect = ChartGraphics.GetPenStyle(series.EmptyPointStyle.BorderDashStyle, series.EmptyPointStyle.BorderWidth); emptyLinePen.StrokeCap = SKStrokeCap.Round; // Check if series is indexed bool indexedSeries = ChartHelper.IndexedSeries(Common, series.Name); // Loop through all ponts in the series int index = 0; double yValueRangeMin = double.NaN; double yValueRangeMax = double.NaN; DataPoint pointRangeMin = null; DataPoint pointRangeMax = null; double xValue = 0; double yValue = 0; double xValuePrev = 0; double yValuePrev = 0; DataPoint prevDataPoint = null; SKPoint lastVerticalSegmentPoint = SKPoint.Empty; SKPoint prevPoint = SKPoint.Empty; SKPoint currentPoint = SKPoint.Empty; bool prevPointInAxesCoordinates = false; bool verticalLineDetected = false; bool prevPointIsEmpty = false; bool currentPointIsEmpty = false; bool firstNonEmptyPoint = false; double xPixelConverter = (graph.Common.ChartPicture.Width - 1.0) / 100.0; double yPixelConverter = (graph.Common.ChartPicture.Height - 1.0) / 100.0; foreach (DataPoint point in series.Points) { // Get point X and Y values xValue = (indexedSeries) ? index + 1 : point.XValue; xValue = hAxis.GetLogValue(xValue); yValue = vAxis.GetLogValue(point.YValues[0]); currentPointIsEmpty = point.IsEmpty; // NOTE: Fixes issue #7094 // If current point is non-empty but the previous one was, // use empty point style properties to draw it. if (prevPointIsEmpty && !currentPointIsEmpty && !firstNonEmptyPoint) { firstNonEmptyPoint = true; currentPointIsEmpty = true; } else { firstNonEmptyPoint = false; } // Check if line is completly out of the data scaleView if (!verticalLineDetected && ((xValue < hAxisMin && xValuePrev < hAxisMin) || (xValue > hAxisMax && xValuePrev > hAxisMax) || (yValue < vAxisMin && yValuePrev < vAxisMin) || (yValue > vAxisMax && yValuePrev > vAxisMax))) { xValuePrev = xValue; yValuePrev = yValue; prevPointInAxesCoordinates = true; ++index; continue; } else if (!clipRegionSet && (xValuePrev < hAxisMin || xValuePrev > hAxisMax || xValue > hAxisMax || xValue < hAxisMin || yValuePrev < vAxisMin || yValuePrev > vAxisMax || yValue < vAxisMin || yValue > vAxisMax)) { // Set clipping region for line drawing graph.SetClip(area.PlotAreaPosition.ToSKRect()); clipRegionSet = true; } // Check if point may be skipped // Check if points X value in acceptable error boundary if (index > 0 && currentPointIsEmpty == prevPointIsEmpty && Math.Abs(xValue - xValuePrev) < axesValuesPixelSizeX) { if (!verticalLineDetected) { verticalLineDetected = true; if (yValue > yValuePrev) { yValueRangeMax = yValue; yValueRangeMin = yValuePrev; pointRangeMax = point; pointRangeMin = prevDataPoint; } else { yValueRangeMax = yValuePrev; yValueRangeMin = yValue; pointRangeMax = prevDataPoint; pointRangeMin = point; } } else { if (yValue > yValueRangeMax) { yValueRangeMax = yValue; pointRangeMax = point; } else if (yValue < yValueRangeMin) { yValueRangeMin = yValue; pointRangeMin = point; } } // Remember last point prevDataPoint = point; // Remember last vertical range point // Note! Point is in axes coordinate. lastVerticalSegmentPoint.Y = (float)yValue; // Increase counter and proceed to next data point ++index; continue; } // Get point pixel position currentPoint.X = (float) (hAxis.GetLinearPosition(xValue) * xPixelConverter); currentPoint.Y = (float) (vAxis.GetLinearPosition(yValue) * yPixelConverter); // Check if previous point must be converted from axes values to pixels if (prevPointInAxesCoordinates) { prevPoint.X = (float) (hAxis.GetLinearPosition(xValuePrev) * xPixelConverter); prevPoint.Y = (float) (vAxis.GetLinearPosition(yValuePrev) * yPixelConverter); } // Draw accumulated vertical line (with minimal X values differences) if (verticalLineDetected) { // Convert Y coordinates to pixels yValueRangeMin = (vAxis.GetLinearPosition(yValueRangeMin) * yPixelConverter); yValueRangeMax = (vAxis.GetLinearPosition(yValueRangeMax) * yPixelConverter); // Draw accumulated vertical line DrawLine( series, prevDataPoint, pointRangeMin, pointRangeMax, index, (prevPointIsEmpty) ? emptyLinePen : linePen, prevPoint.X, (float)yValueRangeMin, prevPoint.X, (float)yValueRangeMax); // Reset vertical line detected flag verticalLineDetected = false; // Convert last point of the vertical line segment to pixel coordinates prevPoint.Y = (float) (vAxis.GetLinearPosition(lastVerticalSegmentPoint.Y) * yPixelConverter); } // Draw line from previous to current point if (index > 0) { DrawLine( series, point, pointRangeMin, pointRangeMax, index, (currentPointIsEmpty) ? emptyLinePen : linePen, prevPoint.X, prevPoint.Y, currentPoint.X, currentPoint.Y); } // Remember last point coordinates xValuePrev = xValue; yValuePrev = yValue; prevDataPoint = point; prevPoint = currentPoint; prevPointInAxesCoordinates = false; prevPointIsEmpty = currentPointIsEmpty; ++index; } // Draw last accumulated line segment if (verticalLineDetected) { // Check if previous point must be converted from axes values to pixels if (prevPointInAxesCoordinates) { prevPoint.X = (float) (hAxis.GetLinearPosition(xValuePrev) * xPixelConverter); prevPoint.Y = (float) (vAxis.GetLinearPosition(yValuePrev) * yPixelConverter); } // Convert Y coordinates to pixels yValueRangeMin = (vAxis.GetLinearPosition(yValueRangeMin) * yPixelConverter); yValueRangeMax = (vAxis.GetLinearPosition(yValueRangeMax) * yPixelConverter); // Draw accumulated vertical line DrawLine( series, prevDataPoint, pointRangeMin, pointRangeMax, index - 1, prevPointIsEmpty ? emptyLinePen : linePen, prevPoint.X, (float)yValueRangeMin, prevPoint.X, (float)yValueRangeMax); } } // Reset Clip Region if (clipRegionSet) { graph.ResetClip(); } }