From 7d96590726ecb19f9b4f05182631e036f43489df Mon Sep 17 00:00:00 2001 From: Paul Anderson Date: Wed, 3 Jun 2026 09:42:57 +0530 Subject: [PATCH] perf: Chart control performance improvements - Replace LINQ .Reverse() with backward for loop in SfCartesianChart - Replace .Where().ToList() with direct iteration in CartesianChartArea - Cache .Values.ToList() outside loops in CartesianChartArea - Replace LINQ query syntax with pre-sized List loops in CartesianSeries - Remove .Where().ToList() allocation in CartesianSeries.UpdateSbsSeries - Cache yPropertyAccessor.Count in data binding loops in ChartSeriesPartial - Cache StrokeDashArray.ToFloatArray() result in AreaSegment - Replace multiple LINQ .Min()/.Max() scans with single loop in AreaSegment.SetData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Charts/Area/Partial/CartesianChartArea.cs | 27 ++++++---- maui/src/Charts/Segment/AreaSegment.cs | 54 ++++++++++++++++--- maui/src/Charts/Series/CartesianSeries.cs | 38 ++++++++++--- maui/src/Charts/Series/ChartSeriesPartial.cs | 9 ++-- maui/src/Charts/SfCartesianChart.cs | 4 +- 5 files changed, 104 insertions(+), 28 deletions(-) diff --git a/maui/src/Charts/Area/Partial/CartesianChartArea.cs b/maui/src/Charts/Area/Partial/CartesianChartArea.cs index 1559fb51..66a46ff0 100644 --- a/maui/src/Charts/Area/Partial/CartesianChartArea.cs +++ b/maui/src/Charts/Area/Partial/CartesianChartArea.cs @@ -182,10 +182,11 @@ internal void UpdateSBS() { double totalWidth = GetTotalWidth() / SideBySideSeriesPosition.Count; double startPosition = 0, end = 0; + var seriesGroupValues = SideBySideSeriesPosition.Values.ToList(); for (int i = 0; i < SideBySideSeriesPosition.Count; i++) { - var seriesGroup = SideBySideSeriesPosition.Values.ToList()[i]; + var seriesGroup = seriesGroupValues[i]; double sbsMaxWidth = GetSBSMaxWidth(seriesGroup); foreach (ChartSeries chartSeries in seriesGroup) @@ -351,15 +352,22 @@ internal void InvalidateMinWidth() internal void ResetSBSSegments() { - var sideBySideSeries = VisibleSeries?.Where(series => series.IsSideBySide).ToList(); - - if (sideBySideSeries != null && sideBySideSeries.Count > 0) + if (VisibleSeries != null) { - SideBySideSeriesPosition = null; + bool hasSideBySide = false; - foreach (var chartSeries in sideBySideSeries) + foreach (var series in VisibleSeries) { - chartSeries.SegmentsCreated = false; + if (series.IsSideBySide) + { + if (!hasSideBySide) + { + hasSideBySide = true; + SideBySideSeriesPosition = null; + } + + series.SegmentsCreated = false; + } } } } @@ -370,10 +378,11 @@ double GetTotalWidth() if (SideBySideSeriesPosition != null) { - for (int i = 0; i < SideBySideSeriesPosition.Count; i++) + var seriesGroupValues = SideBySideSeriesPosition.Values.ToList(); + for (int i = 0; i < seriesGroupValues.Count; i++) { double maxWidth = 0; - foreach (ChartSeries sideBySideSeries in SideBySideSeriesPosition.Values.ToList()[i]) + foreach (ChartSeries sideBySideSeries in seriesGroupValues[i]) { CartesianSeries cartesianSeries = (CartesianSeries)sideBySideSeries; double width = cartesianSeries.GetActualWidth(); diff --git a/maui/src/Charts/Segment/AreaSegment.cs b/maui/src/Charts/Segment/AreaSegment.cs index 4935cd3e..1dd83282 100644 --- a/maui/src/Charts/Segment/AreaSegment.cs +++ b/maui/src/Charts/Segment/AreaSegment.cs @@ -37,6 +37,8 @@ public partial class AreaSegment : CartesianSegment, IMarkerDependentSegment internal List? PreviousStrokePoints { get; set; } = null; PathF? _path; + float[]? _cachedStrokeDashPattern; + DoubleCollection? _previousStrokeDashArray; #endregion @@ -60,7 +62,13 @@ protected internal override void Draw(ICanvas canvas) if (StrokeDashArray != null) { - canvas.StrokeDashPattern = StrokeDashArray.ToFloatArray(); + if (_cachedStrokeDashPattern == null || _previousStrokeDashArray != StrokeDashArray) + { + _cachedStrokeDashPattern = StrokeDashArray.ToFloatArray(); + _previousStrokeDashArray = StrokeDashArray; + } + + canvas.StrokeDashPattern = _cachedStrokeDashPattern; } DrawPath(canvas, FillPoints, StrokePoints); @@ -182,13 +190,47 @@ internal override void SetData(IList xValues, IList yValues) xValues.CopyTo(XValues, 0); yValues.CopyTo(YValues, 0); - var yMin = YValues.Min(); - yMin = double.IsNaN(yMin) ? YValues.Length > 0 ? YValues.Where(e => !double.IsNaN(e)).DefaultIfEmpty().Min() : 0 : yMin; + double yMin = double.MaxValue; + double yMax = double.MinValue; + double xMin = double.MaxValue; + double xMax = double.MinValue; + + for (int i = 0; i < count; i++) + { + double yVal = YValues[i]; + if (!double.IsNaN(yVal)) + { + if (yVal < yMin) yMin = yVal; + if (yVal > yMax) yMax = yVal; + } + + double xVal = XValues[i]; + if (!double.IsNaN(xVal)) + { + if (xVal < xMin) xMin = xVal; + if (xVal > xMax) xMax = xVal; + } + } + + if (yMin == double.MaxValue) + { + Empty = count > 0; + yMin = 0; + yMax = 0; + } + else + { + Empty = false; + } - Empty = double.IsNaN(yMin); + if (xMin == double.MaxValue) + { + xMin = 0; + xMax = 0; + } - series.XRange += new DoubleRange(XValues.Min(), XValues.Max()); - series.YRange += new DoubleRange(yMin, YValues.Max()); + series.XRange += new DoubleRange(xMin, xMax); + series.YRange += new DoubleRange(yMin, yMax); } } diff --git a/maui/src/Charts/Series/CartesianSeries.cs b/maui/src/Charts/Series/CartesianSeries.cs index 5e1e23dc..75f2f1e8 100644 --- a/maui/src/Charts/Series/CartesianSeries.cs +++ b/maui/src/Charts/Series/CartesianSeries.cs @@ -1027,18 +1027,39 @@ internal override void UpdateRange() return null; } - double xIndexValues = 0d; var xValues = ActualXValues as List; if (IsIndexed || xValues == null) { if (ActualXAxis is CategoryAxis categoryAxis && !categoryAxis.ArrangeByIndex || ActualXAxis == null) { - xValues = GroupedXValuesIndexes.Count > 0 ? GroupedXValuesIndexes : (from val in (ActualXValues as List) select (xIndexValues++)).ToList(); + if (GroupedXValuesIndexes.Count > 0) + { + xValues = GroupedXValuesIndexes; + } + else + { + var stringValues = ActualXValues as List; + if (stringValues != null) + { + xValues = new List(stringValues.Count); + for (int i = 0; i < stringValues.Count; i++) + { + xValues.Add(i); + } + } + } } else { - xValues = xValues != null ? (from val in xValues select (xIndexValues++)).ToList() : (from val in (ActualXValues as List) select (xIndexValues++)).ToList(); + int count = xValues != null ? xValues.Count : ((ActualXValues as List)?.Count ?? 0); + var indexedValues = new List(count); + for (int i = 0; i < count; i++) + { + indexedValues.Add(i); + } + + xValues = indexedValues; } } @@ -1133,13 +1154,14 @@ internal void UpdateSbsSeries() { if (ChartArea != null) { - var sideBySideSeries = ChartArea.VisibleSeries?.Where(series => series.IsSideBySide).ToList(); - - if (sideBySideSeries != null && sideBySideSeries.Count > 0) + if (ChartArea.VisibleSeries != null) { - foreach (var chartSeries in sideBySideSeries) + foreach (var series in ChartArea.VisibleSeries) { - chartSeries.SegmentsCreated = false; + if (series.IsSideBySide) + { + series.SegmentsCreated = false; + } } } diff --git a/maui/src/Charts/Series/ChartSeriesPartial.cs b/maui/src/Charts/Series/ChartSeriesPartial.cs index 9bf224cd..56b973c9 100644 --- a/maui/src/Charts/Series/ChartSeriesPartial.cs +++ b/maui/src/Charts/Series/ChartSeriesPartial.cs @@ -824,11 +824,12 @@ internal virtual void GeneratePropertyPoints(string[] yPaths, IList[] yL { if (XValues is List xValue) { + int yPathCount = yPropertyAccessor.Count; do { var xVal = xProperty.GetValue(enumerator.Current); xValue.Add(xVal.Tostring()); - for (int i = 0; i < yPropertyAccessor.Count; i++) + for (int i = 0; i < yPathCount; i++) { var yVal = yPropertyAccessor[i].GetValue(enumerator.Current); yLists[i].Add(Convert.ToDouble(yVal ?? double.NaN)); @@ -844,6 +845,7 @@ internal virtual void GeneratePropertyPoints(string[] yPaths, IList[] yL { if (XValues is List xValue) { + int yPathCount = yPropertyAccessor.Count; do { var xVal = xProperty.GetValue(enumerator.Current); @@ -856,7 +858,7 @@ internal virtual void GeneratePropertyPoints(string[] yPaths, IList[] yL } xValue.Add(XData); - for (int i = 0; i < yPropertyAccessor.Count; i++) + for (int i = 0; i < yPathCount; i++) { var yVal = yPropertyAccessor[i].GetValue(enumerator.Current); yLists[i].Add(Convert.ToDouble(yVal ?? double.NaN)); @@ -872,6 +874,7 @@ internal virtual void GeneratePropertyPoints(string[] yPaths, IList[] yL { if (XValues is List xValue) { + int yPathCount = yPropertyAccessor.Count; do { var xVal = xProperty.GetValue(enumerator.Current); @@ -884,7 +887,7 @@ internal virtual void GeneratePropertyPoints(string[] yPaths, IList[] yL } xValue.Add(XData); - for (int i = 0; i < yPropertyAccessor.Count; i++) + for (int i = 0; i < yPathCount; i++) { var yVal = yPropertyAccessor[i].GetValue(enumerator.Current); yLists[i].Add(Convert.ToDouble(yVal ?? double.NaN)); diff --git a/maui/src/Charts/SfCartesianChart.cs b/maui/src/Charts/SfCartesianChart.cs index 77c8fb8f..b5922184 100644 --- a/maui/src/Charts/SfCartesianChart.cs +++ b/maui/src/Charts/SfCartesianChart.cs @@ -1551,9 +1551,9 @@ internal void OnTapAction(IChart chart, Point tapPoint, int tapCount) var visibleSeries = _chartArea.VisibleSeries; if (visibleSeries != null) { - foreach (var series in visibleSeries.Reverse()) + for (int i = visibleSeries.Count - 1; i >= 0; i--) { - if (series.SelectionHitTest((float)tapPoint.X, (float)tapPoint.Y)) + if (visibleSeries[i].SelectionHitTest((float)tapPoint.X, (float)tapPoint.Y)) { break; }