From bdb80eef4933cd136aa99291ebfff8a5f81dc77e Mon Sep 17 00:00:00 2001 From: Paul Anderson Date: Wed, 27 May 2026 09:14:42 +0530 Subject: [PATCH 1/2] perf: reduce allocations and eliminate LINQ in Chart control hot paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Charts/Area/Partial/CartesianChartArea.cs | 79 ++++++++++++++++--- maui/src/Charts/Layouts/ChartZoomPanView.cs | 12 +-- maui/src/Charts/Series/CartesianSeries.cs | 26 +++++- maui/src/Charts/Series/ChartSeriesPartial.cs | 17 ++-- maui/src/Charts/Series/StackingSeriesBase.cs | 15 ++-- 5 files changed, 112 insertions(+), 37 deletions(-) diff --git a/maui/src/Charts/Area/Partial/CartesianChartArea.cs b/maui/src/Charts/Area/Partial/CartesianChartArea.cs index ad6d14bf..6a99a57f 100644 --- a/maui/src/Charts/Area/Partial/CartesianChartArea.cs +++ b/maui/src/Charts/Area/Partial/CartesianChartArea.cs @@ -102,7 +102,23 @@ internal void CalculateSbsPosition() { if (!stackingSeries.IsSbsValueCalculated && _seriesGroup != null) { - string groupID = _seriesGroup.FirstOrDefault(x => x.Value.Any(s => s.GroupingLabel == stackingSeries.GroupingLabel && s.GetType() == stackingSeries.GetType())).Key; + string? groupID = null; + foreach (var group in _seriesGroup) + { + foreach (var s in group.Value) + { + if (s.GroupingLabel == stackingSeries.GroupingLabel && s.GetType() == stackingSeries.GetType()) + { + groupID = group.Key; + break; + } + } + + if (groupID != null) + { + break; + } + } StackingSeriesBase stackingSeriesBase; int size = SideBySideSeriesPosition.Count > 0 && groupingKeys.Count > 0 && groupingKeys.TryGetValue(groupID, out var groupValue) ? SideBySideSeriesPosition[groupValue].Count : 0; @@ -351,15 +367,23 @@ 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; + return; + } - foreach (var chartSeries in sideBySideSeries) + bool hasSbsSeries = false; + foreach (var series in VisibleSeries) + { + if (series.IsSideBySide) { - chartSeries.SegmentsCreated = false; + if (!hasSbsSeries) + { + SideBySideSeriesPosition = null; + hasSbsSeries = true; + } + + series.SegmentsCreated = false; } } } @@ -370,10 +394,11 @@ double GetTotalWidth() if (SideBySideSeriesPosition != null) { + var positionsList = SideBySideSeriesPosition.Values.ToList(); for (int i = 0; i < SideBySideSeriesPosition.Count; i++) { double maxWidth = 0; - foreach (ChartSeries sideBySideSeries in SideBySideSeriesPosition.Values.ToList()[i]) + foreach (ChartSeries sideBySideSeries in positionsList[i]) { CartesianSeries cartesianSeries = (CartesianSeries)sideBySideSeries; double width = cartesianSeries.GetActualWidth(); @@ -403,7 +428,22 @@ static double GetSBSMaxWidth(List seriesGroup) internal void UpdateStackingSeries() { //if visible series count is 0 or not contain any stacking series then return. - if (VisibleSeries == null || VisibleSeries.Count == 0 || !VisibleSeries.Any(series => series is StackingSeriesBase && !series.SegmentsCreated)) + if (VisibleSeries == null || VisibleSeries.Count == 0) + { + return; + } + + bool hasStackingToCreate = false; + foreach (var series in VisibleSeries) + { + if (series is StackingSeriesBase && !series.SegmentsCreated) + { + hasStackingToCreate = true; + break; + } + } + + if (!hasStackingToCreate) { return; } @@ -488,6 +528,7 @@ static void CalculateStackingValues(Dictionary> var yValues = series.YValues; var bottomValues = new List(); var topValues = new List(); + bool is100Series = series is StackingColumn100Series or StackingLine100Series or StackingArea100Series; if (xValues != null) { @@ -502,7 +543,7 @@ static void CalculateStackingValues(Dictionary> if (positiveYValues.TryGetValue(xValue, out double currentValue)) { bottomValues.Add((axisCross > currentValue) ? axisCross : currentValue); - if (series.GetType().Name.Contains("Stacking", StringComparison.Ordinal) && series.GetType().Name.Contains("100Series", StringComparison.Ordinal)) + if (is100Series) { yValue = GetYValue(seriesList, yValue, i); } @@ -511,7 +552,7 @@ static void CalculateStackingValues(Dictionary> else { bottomValues.Add(axisCross); - if (series.GetType().Name.Contains("Stacking", StringComparison.Ordinal) && series.GetType().Name.Contains("100Series", StringComparison.Ordinal)) + if (is100Series) { yValue = GetYValue(seriesList, yValue, i); } @@ -525,7 +566,7 @@ static void CalculateStackingValues(Dictionary> if (!negativeYValues.TryAdd(xValue, yValue)) { bottomValues.Add((axisCross < negativeYValues[xValue]) ? axisCross : negativeYValues[xValue]); - if (series.GetType().Name.Contains("Stacking", StringComparison.Ordinal) && series.GetType().Name.Contains("100Series", StringComparison.Ordinal)) + if (is100Series) { yValue = GetYValue(seriesList, yValue, i); } @@ -549,7 +590,19 @@ static void CalculateStackingValues(Dictionary> static double GetYValue(List SeriesList, double yValue, int index) { - double total = SeriesList.Where(series => series != null && series.YValues.Count > index).Sum(series => double.IsNaN(series.YValues[index]) ? 0 : Math.Abs(series.YValues[index])); + double total = 0; + foreach (var series in SeriesList) + { + if (series != null && series.YValues.Count > index) + { + double val = series.YValues[index]; + if (!double.IsNaN(val)) + { + total += Math.Abs(val); + } + } + } + if (yValue != 0) { yValue = (yValue / total) * 100; diff --git a/maui/src/Charts/Layouts/ChartZoomPanView.cs b/maui/src/Charts/Layouts/ChartZoomPanView.cs index fa5a08d5..47b3a5a7 100644 --- a/maui/src/Charts/Layouts/ChartZoomPanView.cs +++ b/maui/src/Charts/Layouts/ChartZoomPanView.cs @@ -115,14 +115,16 @@ void GenerateSelectionElements(ICanvas canvas, ObservableCollection a { if (axis.IsVertical) { - startPoint = new Point(isOpposed ? actualArrangeRect.X : actualArrangeRect.X + actualArrangeRect.Width, selectedRect.Top); - endPoint = new Point(isOpposed ? actualArrangeRect.X : actualArrangeRect.X + actualArrangeRect.Width, selectedRect.Bottom); + double xCoord = isOpposed ? actualArrangeRect.X : actualArrangeRect.X + actualArrangeRect.Width; + startPoint = new Point(xCoord, selectedRect.Top); + endPoint = new Point(xCoord, selectedRect.Bottom); tooltipPosition = isOpposed ? TooltipPosition.Right : TooltipPosition.Left; } else { - startPoint = new Point(selectedRect.Left, isOpposed ? actualArrangeRect.Y + actualArrangeRect.Height : actualArrangeRect.Y); - endPoint = new Point(selectedRect.Right, isOpposed ? actualArrangeRect.Y + actualArrangeRect.Height : actualArrangeRect.Y); + double yCoord = isOpposed ? actualArrangeRect.Y + actualArrangeRect.Height : actualArrangeRect.Y; + startPoint = new Point(selectedRect.Left, yCoord); + endPoint = new Point(selectedRect.Right, yCoord); tooltipPosition = isOpposed ? TooltipPosition.Top : TooltipPosition.Bottom; } @@ -177,7 +179,7 @@ void GenerateAxisTrackballInfos(PointF startPoint, PointF endPoint, TooltipPosit ChartZoomPanView.MapChartLabelStyle(chart, axisPointInfo2.Helper, axis.TrackballLabelStyle); } - Rect actualArrangeRect = new Rect(axis.ArrangeRect.X, axis.ArrangeRect.Y, axis.ArrangeRect.X + axis.ArrangeRect.Width, axis.ArrangeRect.Y + axis.ArrangeRect.Height); + Rect actualArrangeRect = new Rect(axisRect.X, axisRect.Y, axisRect.X + axisRect.Width, axisRect.Y + axisRect.Height); axisPointInfo1.Helper.Show(actualArrangeRect, new Rect(startPoint.X - 1, startPoint.Y - 1, _dimension, _dimension), false); axisPointInfo2.Helper.Show(actualArrangeRect, new Rect(endPoint.X - 1, endPoint.Y - 1, _dimension, _dimension), false); diff --git a/maui/src/Charts/Series/CartesianSeries.cs b/maui/src/Charts/Series/CartesianSeries.cs index 5e1e23dc..0bf81606 100644 --- a/maui/src/Charts/Series/CartesianSeries.cs +++ b/maui/src/Charts/Series/CartesianSeries.cs @@ -15,6 +15,7 @@ public abstract class CartesianSeries : ChartSeries double _xAxisMax = double.MinValue; double _yAxisMin = double.MaxValue; double _yAxisMax = double.MinValue; + List? _cachedIndexedXValues; #endregion @@ -1027,26 +1028,44 @@ 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(); + xValues = GroupedXValuesIndexes.Count > 0 ? GroupedXValuesIndexes : GetOrCreateIndexedXValues(); } else { - xValues = xValues != null ? (from val in xValues select (xIndexValues++)).ToList() : (from val in (ActualXValues as List) select (xIndexValues++)).ToList(); + xValues = GetOrCreateIndexedXValues(); } } return xValues; } + List GetOrCreateIndexedXValues() + { + int count = ActualXValues is List dList ? dList.Count : (ActualXValues as List)?.Count ?? 0; + if (_cachedIndexedXValues != null && _cachedIndexedXValues.Count == count) + { + return _cachedIndexedXValues; + } + + var indexedValues = new List(count); + for (int i = 0; i < count; i++) + { + indexedValues.Add(i); + } + + _cachedIndexedXValues = indexedValues; + return _cachedIndexedXValues; + } + internal override void OnDataSourceChanged(object oldValue, object newValue) { + _cachedIndexedXValues = null; ResetAutoScroll(); InvalidateSideBySideSeries(); foreach (var item in EmptyPointIndexes) @@ -1059,6 +1078,7 @@ internal override void OnDataSourceChanged(object oldValue, object newValue) internal override void OnDataSource_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { + _cachedIndexedXValues = null; ResetAutoScroll(); base.OnDataSource_CollectionChanged(sender, e); } diff --git a/maui/src/Charts/Series/ChartSeriesPartial.cs b/maui/src/Charts/Series/ChartSeriesPartial.cs index 9bf224cd..f168e17d 100644 --- a/maui/src/Charts/Series/ChartSeriesPartial.cs +++ b/maui/src/Charts/Series/ChartSeriesPartial.cs @@ -1411,16 +1411,19 @@ void ResetDataPoint() if (ItemsSource != null) { - var items = ItemsSource is IList ? ItemsSource as IList : null; - if (items == null) + bool hasData = false; + if (ItemsSource is IList list) { - if (ItemsSource is IEnumerable source) - { - items = source.Cast().ToList(); - } + hasData = list.Count > 0; + } + else if (ItemsSource is IEnumerable source) + { + var enumerator = source.GetEnumerator(); + hasData = enumerator.MoveNext(); + (enumerator as IDisposable)?.Dispose(); } - if (items != null && items.Count > 0) + if (hasData) { GenerateDataPoints(); } diff --git a/maui/src/Charts/Series/StackingSeriesBase.cs b/maui/src/Charts/Series/StackingSeriesBase.cs index 5d6cd0d4..09c5bfcc 100644 --- a/maui/src/Charts/Series/StackingSeriesBase.cs +++ b/maui/src/Charts/Series/StackingSeriesBase.cs @@ -330,17 +330,14 @@ void RefreshSeries() void ResetVisibleSeries() { - if (ChartArea != null) + if (ChartArea?.VisibleSeries == null) { - var visibleSeries = ChartArea.VisibleSeries; - var stackingSeries = visibleSeries?.Where(series => series.IsStacking).ToList(); - - if (stackingSeries == null) - { - return; - } + return; + } - foreach (var chartSeries in stackingSeries) + foreach (var chartSeries in ChartArea.VisibleSeries) + { + if (chartSeries.IsStacking) { chartSeries.SegmentsCreated = false; } From 628b19198c434c1563fbf5ae07d19c6e1baeedd5 Mon Sep 17 00:00:00 2001 From: SaiyathAliFathima <103025761+SaiyathAliFathima@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:00:02 +0530 Subject: [PATCH 2/2] Update CartesianChartArea.cs Removed unwanted field --- maui/src/Charts/Area/Partial/CartesianChartArea.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/maui/src/Charts/Area/Partial/CartesianChartArea.cs b/maui/src/Charts/Area/Partial/CartesianChartArea.cs index 3ece2aed..fc9e7df3 100644 --- a/maui/src/Charts/Area/Partial/CartesianChartArea.cs +++ b/maui/src/Charts/Area/Partial/CartesianChartArea.cs @@ -532,7 +532,6 @@ static void CalculateStackingValues(Dictionary> var yValues = series.YValues; var bottomValues = new List(); var topValues = new List(); - bool is100Series = series is StackingColumn100Series or StackingLine100Series or StackingArea100Series; if (xValues != null) { @@ -615,4 +614,4 @@ static double GetYValue(List seriesList, double yValue, int } #endregion } -} \ No newline at end of file +}