From e487b4325d9d7d96bef3de548440f6d101044189 Mon Sep 17 00:00:00 2001 From: Paul Anderson Date: Tue, 26 May 2026 12:36:30 +0530 Subject: [PATCH 1/2] =?UTF-8?q?Perf:=20Replace=20O(n=C2=B2)=20lookups=20wi?= =?UTF-8?q?th=20Dictionary=20in=20CategoryAxis.GroupData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GroupData() method used List.Contains() and List.IndexOf() inside loops, resulting in O(n²) time complexity for data grouping. This is especially impactful for charts with large datasets. Changes: - Replace List.Contains() with HashSet.Add() for O(1) deduplication - Build a Dictionary for O(1) index lookups instead of O(n) List.IndexOf() calls per element - Use explicit null-safe pattern matching for ActualXValues - Pre-allocate lists with known capacity Added 5 unit tests covering: - Duplicate values - Double (numeric) X values - Multiple series with overlapping values - Single series - Large dataset (1000+ items) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- maui/src/Charts/Axis/CategoryAxis.cs | 45 +++++- .../Features/ChartFeatureAxisUnitTest.cs | 152 ++++++++++++++++++ 2 files changed, 190 insertions(+), 7 deletions(-) diff --git a/maui/src/Charts/Axis/CategoryAxis.cs b/maui/src/Charts/Axis/CategoryAxis.cs index c45125f2..897917b0 100644 --- a/maui/src/Charts/Axis/CategoryAxis.cs +++ b/maui/src/Charts/Axis/CategoryAxis.cs @@ -33,6 +33,7 @@ internal void GroupData() { List groupingValues = []; List groupedDatas = []; + var groupingSet = new HashSet(StringComparer.Ordinal); foreach (CartesianSeries series in RegisteredSeries.Cast()) { @@ -40,9 +41,9 @@ internal void GroupData() { if (groupedDatas.Count != 0) { - for (int j = 0; j <= xValues.Count - 1; j++) + for (int j = 0; j < xValues.Count; j++) { - if (!groupingValues.Contains(xValues[j])) + if (groupingSet.Add(xValues[j])) { groupingValues.Add(xValues[j]); } @@ -51,11 +52,22 @@ internal void GroupData() else { groupingValues.AddRange(xValues); + foreach (var val in xValues) + { + groupingSet.Add(val); + } } } - else if (series.ActualXValues != null) + else if (series.ActualXValues is List doubleValues) { - groupingValues.AddRange(from val in (series.ActualXValues as List) select val.ToString()); + foreach (var val in doubleValues) + { + var strVal = val.ToString(); + if (groupingSet.Add(strVal)) + { + groupingValues.Add(strVal); + } + } } if (groupingValues.Count != groupedDatas.Count) @@ -66,15 +78,34 @@ internal void GroupData() var distinctXValues = groupingValues.Distinct().ToList(); + // Build an O(1) lookup dictionary for index resolution instead of O(n) IndexOf calls + var indexLookup = new Dictionary(distinctXValues.Count, StringComparer.Ordinal); + for (int i = 0; i < distinctXValues.Count; i++) + { + indexLookup[distinctXValues[i]] = i; + } + foreach (CartesianSeries series in RegisteredSeries.Cast()) { if (series.ActualXValues is List list) { - series.GroupedXValuesIndexes = (from val in list select (double)distinctXValues.IndexOf(val)).ToList(); + var indexes = new List(list.Count); + foreach (var val in list) + { + indexes.Add(indexLookup.TryGetValue(val, out int idx) ? idx : -1); + } + + series.GroupedXValuesIndexes = indexes; } - else if (series.ActualXValues != null) + else if (series.ActualXValues is List doubleList) { - series.GroupedXValuesIndexes = (from val in series.ActualXValues as List select (double)distinctXValues.IndexOf(val.ToString())).ToList(); + var indexes = new List(doubleList.Count); + foreach (var val in doubleList) + { + indexes.Add(indexLookup.TryGetValue(val.ToString(), out int idx) ? idx : -1); + } + + series.GroupedXValuesIndexes = indexes; } series.GroupedXValues = distinctXValues; diff --git a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/ChartFeatureAxisUnitTest.cs b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/ChartFeatureAxisUnitTest.cs index a95537ed..4b3c1551 100644 --- a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/ChartFeatureAxisUnitTest.cs +++ b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/ChartFeatureAxisUnitTest.cs @@ -2505,5 +2505,157 @@ public void PointToValue_ShouldReturnCorrectValue_ForVerticalAxis() #endregion + #region GroupData Performance Tests + + [Fact] + public void GroupData_WithDuplicateValues_ProducesCorrectIndexes() + { + var axis = new CategoryAxis(); + var series1 = new LineSeries + { + ActualXValues = new List { "A", "B", "A", "C", "B" }, + ActualData = [1, 2, 3, 4, 5] + }; + + axis.RegisteredSeries = [series1]; + + axis.GroupData(); + + var expectedDistinctXValues = new List { "A", "B", "C" }; + var expectedIndexes = new List { 0, 1, 0, 2, 1 }; + + Assert.Equal(expectedDistinctXValues, series1.GroupedXValues); + Assert.Equal(expectedIndexes, series1.GroupedXValuesIndexes); + } + + [Fact] + public void GroupData_WithDoubleXValues_ProducesCorrectIndexes() + { + var axis = new CategoryAxis(); + var series1 = new LineSeries + { + ActualXValues = new List { 1.0, 2.0, 3.0 }, + ActualData = [10, 20, 30] + }; + + var series2 = new LineSeries + { + ActualXValues = new List { 2.0, 3.0, 4.0 }, + ActualData = [40, 50, 60] + }; + + axis.RegisteredSeries = [series1, series2]; + + axis.GroupData(); + + var expectedDistinctXValues = new List { "1", "2", "3", "4" }; + var expectedIndexesSeries1 = new List { 0, 1, 2 }; + var expectedIndexesSeries2 = new List { 1, 2, 3 }; + + Assert.Equal(expectedDistinctXValues, series1.GroupedXValues); + Assert.Equal(expectedIndexesSeries1, series1.GroupedXValuesIndexes); + Assert.Equal(expectedIndexesSeries2, series2.GroupedXValuesIndexes); + } + + [Fact] + public void GroupData_WithMultipleSeriesOverlappingValues_DeduplicatesCorrectly() + { + var axis = new CategoryAxis(); + var series1 = new LineSeries + { + ActualXValues = new List { "X", "Y", "Z" }, + ActualData = [1, 2, 3] + }; + + var series2 = new LineSeries + { + ActualXValues = new List { "X", "Y", "Z" }, + ActualData = [4, 5, 6] + }; + + axis.RegisteredSeries = [series1, series2]; + + axis.GroupData(); + + var expectedDistinctXValues = new List { "X", "Y", "Z" }; + var expectedIndexes = new List { 0, 1, 2 }; + + Assert.Equal(expectedDistinctXValues, series1.GroupedXValues); + Assert.Equal(expectedIndexes, series1.GroupedXValuesIndexes); + Assert.Equal(expectedIndexes, series2.GroupedXValuesIndexes); + } + + [Fact] + public void GroupData_WithSingleSeries_ProducesCorrectResults() + { + var axis = new CategoryAxis(); + var series1 = new LineSeries + { + ActualXValues = new List { "Mon", "Tue", "Wed", "Thu", "Fri" }, + ActualData = [10, 20, 30, 40, 50] + }; + + axis.RegisteredSeries = [series1]; + + axis.GroupData(); + + var expectedDistinctXValues = new List { "Mon", "Tue", "Wed", "Thu", "Fri" }; + var expectedIndexes = new List { 0, 1, 2, 3, 4 }; + + Assert.Equal(expectedDistinctXValues, series1.GroupedXValues); + Assert.Equal(expectedIndexes, series1.GroupedXValuesIndexes); + } + + [Fact] + public void GroupData_WithLargeDataset_CompletesWithCorrectResults() + { + var axis = new CategoryAxis(); + int dataSize = 1000; + var xValues1 = new List(dataSize); + var xValues2 = new List(dataSize); + var data1 = new List(dataSize); + var data2 = new List(dataSize); + + for (int i = 0; i < dataSize; i++) + { + xValues1.Add($"Item_{i}"); + data1.Add(i); + } + + for (int i = 500; i < dataSize + 500; i++) + { + xValues2.Add($"Item_{i}"); + data2.Add(i); + } + + var series1 = new LineSeries + { + ActualXValues = xValues1, + ActualData = data1 + }; + + var series2 = new LineSeries + { + ActualXValues = xValues2, + ActualData = data2 + }; + + axis.RegisteredSeries = [series1, series2]; + + axis.GroupData(); + + // Should have 1500 distinct values (0-999 from series1, 1000-1499 from series2) + Assert.Equal(1500, series1.GroupedXValues.Count); + + // First series indexes should be 0..999 + Assert.Equal(0.0, series1.GroupedXValuesIndexes[0]); + Assert.Equal(999.0, series1.GroupedXValuesIndexes[999]); + + // Second series: "Item_500" should map to index 500 + Assert.Equal(500.0, series2.GroupedXValuesIndexes[0]); + } + + #endregion + } } From dedd0fd0bd385763acbe608befa380ce5df74b62 Mon Sep 17 00:00:00 2001 From: SaiyathAliFathima <103025761+SaiyathAliFathima@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:00:26 +0530 Subject: [PATCH 2/2] Update CategoryAxis.cs Resolved conflict --- maui/src/Charts/Axis/CategoryAxis.cs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/maui/src/Charts/Axis/CategoryAxis.cs b/maui/src/Charts/Axis/CategoryAxis.cs index acad34c3..b5292cc1 100644 --- a/maui/src/Charts/Axis/CategoryAxis.cs +++ b/maui/src/Charts/Axis/CategoryAxis.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections; using System.Diagnostics.CodeAnalysis; namespace Syncfusion.Maui.Toolkit.Charts @@ -32,9 +32,8 @@ protected sealed override DoubleRange ApplyRangePadding(DoubleRange range, doubl internal void GroupData() { List groupingValues = []; - var groupingValuesSet = new HashSet(); List groupedDatas = []; - var groupingSet = new HashSet(StringComparer.Ordinal); + var groupingValuesSet = new HashSet(StringComparer.Ordinal); foreach (var item in RegisteredSeries) { @@ -63,7 +62,7 @@ internal void GroupData() } else if (series.ActualXValues is List doubleValues) { - foreach (var val in (series.ActualXValues as List)!) + foreach (var val in doubleValues) { var str = val.ToString(); groupingValues.Add(str); @@ -78,7 +77,7 @@ internal void GroupData() } var distinctXValues = groupingValues.Distinct().ToList(); - var indexMap = new Dictionary(distinctXValues.Count); + var indexMap = new Dictionary(distinctXValues.Count, StringComparer.Ordinal); for (int i = 0; i < distinctXValues.Count; i++) { indexMap[distinctXValues[i]] = i; @@ -90,11 +89,23 @@ internal void GroupData() if (series.ActualXValues is List list) { - series.GroupedXValuesIndexes = list.Select(val => (double)indexMap[val]).ToList(); + var indexes = new List(list.Count); + foreach (var val in list) + { + indexes.Add(indexMap.TryGetValue(val, out int idx) ? idx : -1); + } + + series.GroupedXValuesIndexes = indexes; } else if (series.ActualXValues is List doubleList) { - series.GroupedXValuesIndexes = (series.ActualXValues as List)!.Select(val => (double)indexMap[val.ToString()]).ToList(); + var indexes = new List(doubleList.Count); + foreach (var val in doubleList) + { + indexes.Add(indexMap.TryGetValue(val.ToString(), out int idx) ? idx : -1); + } + + series.GroupedXValuesIndexes = indexes; } series.GroupedXValues = distinctXValues;