From a229369c3c1370210ded11b0a46b23e28d1d207b Mon Sep 17 00:00:00 2001 From: Paul Anderson Date: Sat, 11 Jul 2026 10:22:28 +0530 Subject: [PATCH 1/5] perf(Charts): Consolidate 10 performance improvements in Chart control 1. Replace string-based double comparison with Math.Abs epsilon check (ChartDataUtils.cs) 2. Add direct type checks in ConvertToDouble to avoid redundant ToString() allocations (ChartUtils.cs) 3. Materialize OfType() result to prevent repeated LINQ evaluation (CartesianChartArea.cs) 4. Remove unnecessary .ToList() on Dictionary.Values - use foreach directly (CartesianChartArea.cs) 5. Eliminate unnecessary list copies in DataPointSelectionBehavior.ResetMultiSelection 6. Eliminate unnecessary list copies in SeriesSelectionBehavior.ResetMultiSelection 7. Use HashSet for O(1) lookups in trackball point filtering (ChartTrackballBehavior.cs) 8. Replace Cast() LINQ wrapper with direct iteration in rendering loops (CircularSeries, FinancialSeriesBase, StepAreaSeries, AreaSeries, ScatterSeries, RangeAreaSeries) 9. Remove unnecessary .ToList() materialization in CartesianSeries.UpdateSbsSeries 10. Replace separate Min()/Max() LINQ calls with single-pass loop (BoxAndWhiskerSeries) These changes reduce memory allocations, eliminate redundant LINQ evaluations, and improve CPU efficiency in hot rendering and interaction paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Charts/Area/Partial/CartesianChartArea.cs | 13 ++++----- .../Behaviors/ChartTrackballBehavior.cs | 3 +- .../Behaviors/DataPointSelectionBehavior.cs | 12 +++++--- .../Behaviors/SeriesSelectionBehavior.cs | 9 +++--- maui/src/Charts/Series/AreaSeries.cs | 3 +- maui/src/Charts/Series/BoxAndWhiskerSeries.cs | 14 ++++++++-- maui/src/Charts/Series/CartesianSeries.cs | 4 +-- maui/src/Charts/Series/CircularSeries.cs | 3 +- maui/src/Charts/Series/FinancialSeriesBase.cs | 3 +- maui/src/Charts/Series/RangeAreaSeries.cs | 3 +- maui/src/Charts/Series/ScatterSeries.cs | 5 ++-- maui/src/Charts/Series/StepAreaSeries.cs | 3 +- maui/src/Charts/Utils/ChartDataUtils.cs | 2 +- maui/src/Charts/Utils/ChartUtils.cs | 28 +++++++++++++++++-- 14 files changed, 74 insertions(+), 31 deletions(-) diff --git a/maui/src/Charts/Area/Partial/CartesianChartArea.cs b/maui/src/Charts/Area/Partial/CartesianChartArea.cs index e3933034..3ce42753 100644 --- a/maui/src/Charts/Area/Partial/CartesianChartArea.cs +++ b/maui/src/Charts/Area/Partial/CartesianChartArea.cs @@ -61,7 +61,7 @@ internal void CalculateSbsPosition() } SideBySideSeriesPosition = []; - var cartesianSeriesCollection = visibleSeries.OfType(); + var cartesianSeriesCollection = visibleSeries.OfType().ToList(); foreach (var cartesianSeries in cartesianSeriesCollection) { @@ -200,10 +200,9 @@ internal void UpdateSBS() double totalWidth = GetTotalWidth() / SideBySideSeriesPosition.Count; double startPosition = 0, end = 0; - var sideBySideValues = SideBySideSeriesPosition.Values.ToList(); - for (int i = 0; i < sideBySideValues.Count; i++) + var sideBySideValues = SideBySideSeriesPosition.Values; + foreach (var seriesGroup in sideBySideValues) { - var seriesGroup = sideBySideValues[i]; double sbsMaxWidth = GetSBSMaxWidth(seriesGroup); foreach (ChartSeries chartSeries in seriesGroup) @@ -396,11 +395,11 @@ double GetTotalWidth() if (SideBySideSeriesPosition != null) { - var sideBySideValues = SideBySideSeriesPosition.Values.ToList(); - for (int i = 0; i < sideBySideValues.Count; i++) + var sideBySideValues = SideBySideSeriesPosition.Values; + foreach (var seriesGroup in sideBySideValues) { double maxWidth = 0; - foreach (ChartSeries sideBySideSeries in sideBySideValues[i]) + foreach (ChartSeries sideBySideSeries in seriesGroup) { CartesianSeries cartesianSeries = (CartesianSeries)sideBySideSeries; double width = cartesianSeries.GetActualWidth(); diff --git a/maui/src/Charts/Behaviors/ChartTrackballBehavior.cs b/maui/src/Charts/Behaviors/ChartTrackballBehavior.cs index e745b40e..2636917d 100644 --- a/maui/src/Charts/Behaviors/ChartTrackballBehavior.cs +++ b/maui/src/Charts/Behaviors/ChartTrackballBehavior.cs @@ -1707,11 +1707,12 @@ float FindLeastXValue(float pointX, float pointY) } + var leastXPointsSet = new HashSet(leastXPointsInfo); var copyList = PointInfos.ToList(); foreach (var pointInfo in copyList) { - if (!leastXPointsInfo.Contains(pointInfo)) + if (!leastXPointsSet.Contains(pointInfo)) { RemoveTrackballInfo(pointInfo); } diff --git a/maui/src/Charts/Behaviors/DataPointSelectionBehavior.cs b/maui/src/Charts/Behaviors/DataPointSelectionBehavior.cs index d4468dfe..a53f4986 100644 --- a/maui/src/Charts/Behaviors/DataPointSelectionBehavior.cs +++ b/maui/src/Charts/Behaviors/DataPointSelectionBehavior.cs @@ -67,10 +67,12 @@ internal override void ResetMultiSelection() { if (Source != null) { - var selectedIndexes = ActualSelectedIndexes.ToList(); - ActualSelectedIndexes.Clear(); - foreach (var index in selectedIndexes) + int count = ActualSelectedIndexes.Count; + bool needsInvalidate = count > 0; + + for (int i = count - 1; i >= 0; i--) { + int index = ActualSelectedIndexes[i]; if (index < Source.Segments.Count && index > -1) { Source.SetFillColor(Source.Segments[index]); @@ -78,7 +80,9 @@ internal override void ResetMultiSelection() } } - if (selectedIndexes.Count > 0) + ActualSelectedIndexes.Clear(); + + if (needsInvalidate) { Source.Invalidate(); } diff --git a/maui/src/Charts/Behaviors/SeriesSelectionBehavior.cs b/maui/src/Charts/Behaviors/SeriesSelectionBehavior.cs index ef593595..3efeda43 100644 --- a/maui/src/Charts/Behaviors/SeriesSelectionBehavior.cs +++ b/maui/src/Charts/Behaviors/SeriesSelectionBehavior.cs @@ -91,12 +91,13 @@ internal override void UpdateSelectedItem(int index) internal override void ResetMultiSelection() { - var selectedIndexes = ActualSelectedIndexes.ToList(); - ActualSelectedIndexes.Clear(); - foreach (var index in selectedIndexes) + int count = ActualSelectedIndexes.Count; + for (int i = count - 1; i >= 0; i--) { - UpdateSelectedItem(index); + UpdateSelectedItem(ActualSelectedIndexes[i]); } + + ActualSelectedIndexes.Clear(); } internal override void SelectionIndexChanged(int oldValue, int newValue) diff --git a/maui/src/Charts/Series/AreaSeries.cs b/maui/src/Charts/Series/AreaSeries.cs index 8404322b..9a2e8d40 100644 --- a/maui/src/Charts/Series/AreaSeries.cs +++ b/maui/src/Charts/Series/AreaSeries.cs @@ -439,8 +439,9 @@ public override int GetDataPointIndex(float pointX, float pointY) } List segPoints = []; - foreach (AreaSegment segment in _segments.Cast()) + foreach (ChartSegment item in _segments) { + AreaSegment segment = (AreaSegment)item; var points = segment.FillPoints; if (points != null) { diff --git a/maui/src/Charts/Series/BoxAndWhiskerSeries.cs b/maui/src/Charts/Series/BoxAndWhiskerSeries.cs index 4af36d7d..4b81d928 100644 --- a/maui/src/Charts/Series/BoxAndWhiskerSeries.cs +++ b/maui/src/Charts/Series/BoxAndWhiskerSeries.cs @@ -1103,8 +1103,18 @@ internal override void GenerateSegments(SeriesView seriesView) if (outliers.Count > 0) { - actualMinimum = Math.Min(outliers.Min(), actualMinimum); - actualMaximum = Math.Max(outliers.Max(), actualMaximum); + for (int j = 0; j < outliers.Count; j++) + { + if (outliers[j] < actualMinimum) + { + actualMinimum = outliers[j]; + } + + if (outliers[j] > actualMaximum) + { + actualMaximum = outliers[j]; + } + } } if (xValues != null) diff --git a/maui/src/Charts/Series/CartesianSeries.cs b/maui/src/Charts/Series/CartesianSeries.cs index 0bf81606..5728599d 100644 --- a/maui/src/Charts/Series/CartesianSeries.cs +++ b/maui/src/Charts/Series/CartesianSeries.cs @@ -1153,9 +1153,9 @@ internal void UpdateSbsSeries() { if (ChartArea != null) { - var sideBySideSeries = ChartArea.VisibleSeries?.Where(series => series.IsSideBySide).ToList(); + var sideBySideSeries = ChartArea.VisibleSeries?.Where(series => series.IsSideBySide); - if (sideBySideSeries != null && sideBySideSeries.Count > 0) + if (sideBySideSeries != null && sideBySideSeries.Any()) { foreach (var chartSeries in sideBySideSeries) { diff --git a/maui/src/Charts/Series/CircularSeries.cs b/maui/src/Charts/Series/CircularSeries.cs index fe50c9cf..6f115640 100644 --- a/maui/src/Charts/Series/CircularSeries.cs +++ b/maui/src/Charts/Series/CircularSeries.cs @@ -534,8 +534,9 @@ protected internal override void DrawDataLabel(ICanvas canvas, Brush? fillColor, internal void UpdateDataLabelPositions(ICanvas canvas) { - foreach (PieSegment segment in _segments.Cast()) + foreach (ChartSegment item in _segments) { + PieSegment segment = (PieSegment)item; if (IsSegmentWithinBounds(segment) && segment.IsVisible) { DrawConnectorLine(canvas, segment); diff --git a/maui/src/Charts/Series/FinancialSeriesBase.cs b/maui/src/Charts/Series/FinancialSeriesBase.cs index 057d7a7b..7a1cc1fa 100644 --- a/maui/src/Charts/Series/FinancialSeriesBase.cs +++ b/maui/src/Charts/Series/FinancialSeriesBase.cs @@ -700,8 +700,9 @@ internal override void DrawDataLabels(ICanvas canvas) ChartDataLabelStyle labelStyle = dataLabelSettings.LabelStyle; - foreach (HiLoOpenCloseSegment dataLabel in _segments.Cast()) + foreach (ChartSegment item in _segments) { + HiLoOpenCloseSegment dataLabel = (HiLoOpenCloseSegment)item; if (!dataLabel.InVisibleRange || dataLabel.IsZero) { continue; diff --git a/maui/src/Charts/Series/RangeAreaSeries.cs b/maui/src/Charts/Series/RangeAreaSeries.cs index 0372b7a9..54ba2ddb 100644 --- a/maui/src/Charts/Series/RangeAreaSeries.cs +++ b/maui/src/Charts/Series/RangeAreaSeries.cs @@ -558,8 +558,9 @@ internal override void DrawDataLabels(ICanvas canvas) ChartDataLabelStyle labelStyle = DataLabelSettings.LabelStyle; - foreach (RangeAreaSegment dataLabel in _segments.Cast()) + foreach (ChartSegment item in _segments) { + RangeAreaSegment dataLabel = (RangeAreaSegment)item; if (dataLabel == null || dataLabel.XValues == null || dataLabel.HighValues == null || dataLabel.LowValues == null) { return; diff --git a/maui/src/Charts/Series/ScatterSeries.cs b/maui/src/Charts/Series/ScatterSeries.cs index 6222ad16..e9b9b216 100644 --- a/maui/src/Charts/Series/ScatterSeries.cs +++ b/maui/src/Charts/Series/ScatterSeries.cs @@ -492,9 +492,10 @@ void CreateSegment(SeriesView seriesView, double x, double y, int index) void UpdateScatterType(ShapeType type) { - foreach (ScatterSegment item in _segments.Cast()) + foreach (ChartSegment item in _segments) { - item.Type = type; + ScatterSegment scatterItem = (ScatterSegment)item; + scatterItem.Type = type; } } diff --git a/maui/src/Charts/Series/StepAreaSeries.cs b/maui/src/Charts/Series/StepAreaSeries.cs index 801bb5bb..08ec1fd1 100644 --- a/maui/src/Charts/Series/StepAreaSeries.cs +++ b/maui/src/Charts/Series/StepAreaSeries.cs @@ -92,8 +92,9 @@ public override int GetDataPointIndex(float pointX, float pointY) List segPoints = []; - foreach (StepAreaSegment segment in _segments.Cast()) + foreach (ChartSegment item in _segments) { + StepAreaSegment segment = (StepAreaSegment)item; var points = segment.FillPoints; if (points != null) { diff --git a/maui/src/Charts/Utils/ChartDataUtils.cs b/maui/src/Charts/Utils/ChartDataUtils.cs index 5f9d57aa..73092fc9 100644 --- a/maui/src/Charts/Utils/ChartDataUtils.cs +++ b/maui/src/Charts/Utils/ChartDataUtils.cs @@ -28,7 +28,7 @@ internal class ChartDataUtils internal static bool EqualDoubleValues(double x, double y) { - return x.ToString() == y.ToString(); + return Math.Abs(x - y) < 1e-10; } internal static DateTime IncreaseInterval(DateTime date, double visibleInterval, DateTimeIntervalType actualIntervalType) diff --git a/maui/src/Charts/Utils/ChartUtils.cs b/maui/src/Charts/Utils/ChartUtils.cs index 86ca5bfc..dbc4869c 100644 --- a/maui/src/Charts/Utils/ChartUtils.cs +++ b/maui/src/Charts/Utils/ChartUtils.cs @@ -50,14 +50,36 @@ internal static double ConvertToDouble(object? val) return double.NaN; } - if (double.TryParse(val.ToString(), out double doubleVal)) + if (val is double d) + { + return d; + } + + if (val is float f) + { + return f; + } + + if (val is int i) + { + return i; + } + + if (val is long l) + { + return l; + } + + string? strVal = val.ToString(); + + if (double.TryParse(strVal, out double doubleVal)) { return doubleVal; } //Maui-884 The Following date time formats was acceptable for CrossesAt value. ("MM/dd/yyyy"),("dddd, dd MMMM yyyy"),("dddd, dd MMMM yyyy HH:mm:ss"),("MM/dd/yyyy HH:mm"),("MM/dd/yyyy hh:mm tt"),("MM/dd/yyyy H:mm"),("MM/dd/yyyy h:mm tt"),("MM/dd/yyyy HH:mm:ss"), - //("MMMM dd"),("yyyy’-‘MM’-‘dd’T’HH’:’mm’:’ss.fffffffK"),("ddd, dd MMM yyy HH’:’mm’:’ss ‘GMT’"),("yyyy’-‘MM’-‘dd’T’HH’:’mm’:’ss"),("HH:mm"),("hh:mm tt"),("H:mm"),("h:mm tt"),("HH:mm:ss"),("yyyy MMMM") . - if (DateTime.TryParse(val.ToString(), out DateTime date)) + //("MMMM dd"),("yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK"),("ddd, dd MMM yyy HH':'mm':'ss 'GMT'"),("yyyy'-'MM'-'dd'T'HH':'mm':'ss"),("HH:mm"),("hh:mm tt"),("H:mm"),("h:mm tt"),("HH:mm:ss"),("yyyy MMMM") . + if (DateTime.TryParse(strVal, out DateTime date)) { if (date == DateTime.MaxValue) { From cbdece4279395b8840ca3856ec272771402b9643 Mon Sep 17 00:00:00 2001 From: SaiyathAliFathima <103025761+SaiyathAliFathima@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:17:55 +0530 Subject: [PATCH 2/5] Update DataPointSelectionBehavior.cs --- .../Charts/Behaviors/DataPointSelectionBehavior.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/maui/src/Charts/Behaviors/DataPointSelectionBehavior.cs b/maui/src/Charts/Behaviors/DataPointSelectionBehavior.cs index a53f4986..bcd00c10 100644 --- a/maui/src/Charts/Behaviors/DataPointSelectionBehavior.cs +++ b/maui/src/Charts/Behaviors/DataPointSelectionBehavior.cs @@ -67,12 +67,10 @@ internal override void ResetMultiSelection() { if (Source != null) { - int count = ActualSelectedIndexes.Count; - bool needsInvalidate = count > 0; - - for (int i = count - 1; i >= 0; i--) + var selectedIndexes = ActualSelectedIndexes.ToList(); + ActualSelectedIndexes.Clear(); + foreach (var index in selectedIndexes) { - int index = ActualSelectedIndexes[i]; if (index < Source.Segments.Count && index > -1) { Source.SetFillColor(Source.Segments[index]); @@ -80,9 +78,7 @@ internal override void ResetMultiSelection() } } - ActualSelectedIndexes.Clear(); - - if (needsInvalidate) + if (selectedIndexes.Count > 0) { Source.Invalidate(); } @@ -129,4 +125,4 @@ internal override void SelectionIndexesPropertyChanged(List newValue) #endregion } -} \ No newline at end of file +} From fd1757654e016a7fc38ec8fa1a59cce0393213cf Mon Sep 17 00:00:00 2001 From: SaiyathAliFathima <103025761+SaiyathAliFathima@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:18:34 +0530 Subject: [PATCH 3/5] Update SeriesSelectionBehavior.cs Reverted --- maui/src/Charts/Behaviors/SeriesSelectionBehavior.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/maui/src/Charts/Behaviors/SeriesSelectionBehavior.cs b/maui/src/Charts/Behaviors/SeriesSelectionBehavior.cs index 3efeda43..ed02834e 100644 --- a/maui/src/Charts/Behaviors/SeriesSelectionBehavior.cs +++ b/maui/src/Charts/Behaviors/SeriesSelectionBehavior.cs @@ -91,13 +91,12 @@ internal override void UpdateSelectedItem(int index) internal override void ResetMultiSelection() { - int count = ActualSelectedIndexes.Count; - for (int i = count - 1; i >= 0; i--) + var selectedIndexes = ActualSelectedIndexes.ToList(); + ActualSelectedIndexes.Clear(); + foreach (var index in selectedIndexes) { - UpdateSelectedItem(ActualSelectedIndexes[i]); + UpdateSelectedItem(index); } - - ActualSelectedIndexes.Clear(); } internal override void SelectionIndexChanged(int oldValue, int newValue) @@ -136,4 +135,4 @@ internal override void SelectionIndexesPropertyChanged(List newValue) #endregion } -} \ No newline at end of file +} From 77e92f3d3beb02f2f79d4fed9003bdd6560d2411 Mon Sep 17 00:00:00 2001 From: SaiyathAliFathima <103025761+SaiyathAliFathima@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:19:20 +0530 Subject: [PATCH 4/5] Update ChartUtils.cs Reverted --- maui/src/Charts/Utils/ChartUtils.cs | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/maui/src/Charts/Utils/ChartUtils.cs b/maui/src/Charts/Utils/ChartUtils.cs index 3e8441ed..2492391e 100644 --- a/maui/src/Charts/Utils/ChartUtils.cs +++ b/maui/src/Charts/Utils/ChartUtils.cs @@ -50,36 +50,14 @@ internal static double ConvertToDouble(object? val) return double.NaN; } - if (val is double d) - { - return d; - } - - if (val is float f) - { - return f; - } - - if (val is int i) - { - return i; - } - - if (val is long l) - { - return l; - } - - string? strVal = val.ToString(); - - if (double.TryParse(strVal, out double doubleVal)) + if (double.TryParse(val.ToString(), out double doubleVal)) { return doubleVal; } //Maui-884 The Following date time formats was acceptable for CrossesAt value. ("MM/dd/yyyy"),("dddd, dd MMMM yyyy"),("dddd, dd MMMM yyyy HH:mm:ss"),("MM/dd/yyyy HH:mm"),("MM/dd/yyyy hh:mm tt"),("MM/dd/yyyy H:mm"),("MM/dd/yyyy h:mm tt"),("MM/dd/yyyy HH:mm:ss"), - //("MMMM dd"),("yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK"),("ddd, dd MMM yyy HH':'mm':'ss 'GMT'"),("yyyy'-'MM'-'dd'T'HH':'mm':'ss"),("HH:mm"),("hh:mm tt"),("H:mm"),("h:mm tt"),("HH:mm:ss"),("yyyy MMMM") . - if (DateTime.TryParse(strVal, out DateTime date)) + //("MMMM dd"),("yyyy’-‘MM’-‘dd’T’HH’:’mm’:’ss.fffffffK"),("ddd, dd MMM yyy HH’:’mm’:’ss ‘GMT’"),("yyyy’-‘MM’-‘dd’T’HH’:’mm’:’ss"),("HH:mm"),("hh:mm tt"),("H:mm"),("h:mm tt"),("HH:mm:ss"),("yyyy MMMM") . + if (DateTime.TryParse(val.ToString(), out DateTime date)) { if (date == DateTime.MaxValue) { From 4b5943ed117a407917302c78d1910e71f30d63e0 Mon Sep 17 00:00:00 2001 From: SaiyathAliFathima <103025761+SaiyathAliFathima@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:22:35 +0530 Subject: [PATCH 5/5] Update ChartDataUtils.cs Reverted --- maui/src/Charts/Utils/ChartDataUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maui/src/Charts/Utils/ChartDataUtils.cs b/maui/src/Charts/Utils/ChartDataUtils.cs index 73092fc9..5f9d57aa 100644 --- a/maui/src/Charts/Utils/ChartDataUtils.cs +++ b/maui/src/Charts/Utils/ChartDataUtils.cs @@ -28,7 +28,7 @@ internal class ChartDataUtils internal static bool EqualDoubleValues(double x, double y) { - return Math.Abs(x - y) < 1e-10; + return x.ToString() == y.ToString(); } internal static DateTime IncreaseInterval(DateTime date, double visibleInterval, DateTimeIntervalType actualIntervalType)