From 45b7269955761b8adc08c935fc73c965909ba111 Mon Sep 17 00:00:00 2001 From: Harsha Date: Tue, 11 Mar 2025 21:27:29 +0530 Subject: [PATCH 1/5] Added Empty points SB --- .../SampleList/CartesianChartSamplesList.xml | 6 +- .../EmptyPoints/EmptyPointSupport.xaml | 120 ++++++ .../EmptyPoints/EmptyPointSupport.xaml.cs | 39 ++ .../EmptyPoints/EmptyPointViewModel.cs | 43 ++ .../Charts/Area/Partial/CartesianChartArea.cs | 9 + .../Axis/Layouts/CartesianAxisLayout.cs | 10 + maui/src/Charts/Layouts/ChartSeriesView.cs | 1 + maui/src/Charts/Segment/CartesianSegment.cs | 13 +- maui/src/Charts/Segment/ChartSegment.cs | 2 +- maui/src/Charts/Segment/FastLineSegment.cs | 2 +- .../Charts/Segment/HiLoOpenCloseSegment.cs | 2 +- maui/src/Charts/Segment/PieSegment.cs | 2 +- maui/src/Charts/Segment/RangeAreaSegment.cs | 4 +- maui/src/Charts/Segment/RangeColumnSegment.cs | 2 +- .../Charts/Segment/SplineRangeAreaSegment.cs | 4 +- maui/src/Charts/Series/AreaSeries.cs | 2 +- maui/src/Charts/Series/BubbleSeries.cs | 39 ++ maui/src/Charts/Series/CartesianSeries.cs | 382 ++++++++++++++++++ maui/src/Charts/Series/ChartSeries.cs | 11 +- maui/src/Charts/Series/ChartSeriesPartial.cs | 14 +- maui/src/Charts/Series/ErrorBarSeries.cs | 2 + maui/src/Charts/Series/FastLineSeries.cs | 2 + maui/src/Charts/Series/FinancialSeriesBase.cs | 65 ++- maui/src/Charts/Series/HiLoOpenCloseSeries.cs | 2 +- maui/src/Charts/Series/RangeAreaSeries.cs | 2 +- maui/src/Charts/Series/RangeColumnSeries.cs | 4 +- maui/src/Charts/Series/RangeSeriesBase .cs | 39 ++ .../Charts/Series/SplineRangeAreaSeries.cs | 2 +- maui/src/Charts/Series/WaterfallSeries.cs | 2 +- maui/src/Charts/Series/XYDataSeries.cs | 31 ++ maui/src/Charts/Styles/EmptyPointSettings.cs | 293 ++++++++++++++ maui/src/Charts/Utils/Enum.cs | 24 ++ .../Features/EmptyPointSettingUnitTest.cs | 219 ++++++++++ 33 files changed, 1366 insertions(+), 28 deletions(-) create mode 100644 maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointSupport.xaml create mode 100644 maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointSupport.xaml.cs create mode 100644 maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointViewModel.cs create mode 100644 maui/src/Charts/Styles/EmptyPointSettings.cs create mode 100644 maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/EmptyPointSettingUnitTest.cs diff --git a/maui/samples/Gallery/SampleList/CartesianChartSamplesList.xml b/maui/samples/Gallery/SampleList/CartesianChartSamplesList.xml index 466a3875..864d6c9f 100644 --- a/maui/samples/Gallery/SampleList/CartesianChartSamplesList.xml +++ b/maui/samples/Gallery/SampleList/CartesianChartSamplesList.xml @@ -1,7 +1,7 @@  - + @@ -57,6 +57,8 @@ + + @@ -170,6 +172,8 @@ + + diff --git a/maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointSupport.xaml b/maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointSupport.xaml new file mode 100644 index 00000000..5b7636cf --- /dev/null +++ b/maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointSupport.xaml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointSupport.xaml.cs b/maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointSupport.xaml.cs new file mode 100644 index 00000000..9518d217 --- /dev/null +++ b/maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointSupport.xaml.cs @@ -0,0 +1,39 @@ + +using Syncfusion.Maui.Toolkit.Charts; + +namespace Syncfusion.Maui.ControlsGallery.CartesianChart.SfCartesianChart +{ + public partial class EmptyPointSupport : SampleView + { + public EmptyPointSupport() + { + InitializeComponent(); + } + + private void picker_SelectedIndexChanged(object sender, EventArgs e) + { + var picker = (Picker)sender; + int selectedIndex = picker.SelectedIndex; + switch (selectedIndex) + { + case 1: + ViewModel.EmptyPointMode = EmptyPointMode.Zero; + break; + + case 2: + ViewModel.EmptyPointMode = EmptyPointMode.Average; + break; + + default: + ViewModel.EmptyPointMode = EmptyPointMode.None; + break; + } + } + + public override void OnDisappearing() + { + base.OnDisappearing(); + Chart.Handler?.DisconnectHandler(); + } + } +} diff --git a/maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointViewModel.cs b/maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointViewModel.cs new file mode 100644 index 00000000..7e15c684 --- /dev/null +++ b/maui/samples/Gallery/Samples/CartesianChart/EmptyPoints/EmptyPointViewModel.cs @@ -0,0 +1,43 @@ + +using System.Collections.ObjectModel; +using Syncfusion.Maui.Toolkit.Charts; + +namespace Syncfusion.Maui.ControlsGallery.CartesianChart.SfCartesianChart +{ + public class EmptyPointViewModel : BaseViewModel + { + private EmptyPointMode emptypointMode; + public EmptyPointMode EmptyPointMode + { + get => emptypointMode; + set + { + if (emptypointMode != value) + { + emptypointMode = value; + OnPropertyChanged(nameof(EmptyPointMode)); + } + } + } + + public string[] EmptyPointModeValues => ["None", "Zero", "Average"]; + + public ObservableCollection EmptyPointData { get; set; } + + public EmptyPointViewModel() + { + + EmptyPointData = + [ + new ChartDataModel("Tropical", 85, 70, 50), + new ChartDataModel("Continental", 80, 65, 40), + new ChartDataModel("Mediterranean", 82, 60, 30), + new ChartDataModel("Arid", double.NaN, double.NaN, double.NaN), + new ChartDataModel("Polar", double.NaN, double.NaN, double.NaN), + new ChartDataModel("Temperate", 75, 50, 40), + new ChartDataModel("Oceanic", 90, 65, 40), + new ChartDataModel("Highland", 95, 60, 30), + ]; + } + } +} diff --git a/maui/src/Charts/Area/Partial/CartesianChartArea.cs b/maui/src/Charts/Area/Partial/CartesianChartArea.cs index 6b71b542..93784765 100644 --- a/maui/src/Charts/Area/Partial/CartesianChartArea.cs +++ b/maui/src/Charts/Area/Partial/CartesianChartArea.cs @@ -414,6 +414,15 @@ internal void UpdateStackingSeries() { if (series is StackingSeriesBase stackingSeries && stackingSeries.IsVisible) { + + if (stackingSeries.RequiredEmptyPointReset) + { + stackingSeries.ResetEmptyPointIndexes(); + stackingSeries.RequiredEmptyPointReset = false; + } + + stackingSeries.ValidateYValues(); + var stackingGroup = stackingSeries.GroupingLabel; var stackingXAxis = stackingSeries.ActualXAxis; var stackingYAxis = stackingSeries.ActualYAxis; diff --git a/maui/src/Charts/Axis/Layouts/CartesianAxisLayout.cs b/maui/src/Charts/Axis/Layouts/CartesianAxisLayout.cs index 84883cf6..4efaa865 100644 --- a/maui/src/Charts/Axis/Layouts/CartesianAxisLayout.cs +++ b/maui/src/Charts/Axis/Layouts/CartesianAxisLayout.cs @@ -414,6 +414,16 @@ void UpdateSeriesRange(ReadOnlyObservableCollection visibleSeries) { foreach (CartesianSeries series in visibleSeries.Cast()) { + if (!series.IsStacking) + { + if (series.RequiredEmptyPointReset) + { + series.ResetEmptyPointIndexes(); + series.RequiredEmptyPointReset = false; + } + + series.ValidateYValues(); + } if (!series.SegmentsCreated) //creates segment if segmentsCreated is false. { diff --git a/maui/src/Charts/Layouts/ChartSeriesView.cs b/maui/src/Charts/Layouts/ChartSeriesView.cs index d3f6205a..b611044c 100644 --- a/maui/src/Charts/Layouts/ChartSeriesView.cs +++ b/maui/src/Charts/Layouts/ChartSeriesView.cs @@ -118,6 +118,7 @@ internal void InternalCreateSegments() _series.OldSegments = null; } + _series.UpdateEmptyPointSettings(); _series.SegmentsCreated = true; } } diff --git a/maui/src/Charts/Segment/CartesianSegment.cs b/maui/src/Charts/Segment/CartesianSegment.cs index 37da838e..ee1d214d 100644 --- a/maui/src/Charts/Segment/CartesianSegment.cs +++ b/maui/src/Charts/Segment/CartesianSegment.cs @@ -7,6 +7,15 @@ /// public abstract class CartesianSegment : ChartSegment { + #region Property + + /// + /// Get the bool value to identify the empty point segment. + /// + public bool IsEmpty { get; internal set; } + + #endregion + #region Methods #region Animation Methods @@ -53,7 +62,7 @@ internal void CalculateDataLabelPosition(double xValue, double yValue, double ac return; } - IsEmpty = double.IsNaN(yValue); + IsZero = double.IsNaN(yValue); double x = xValue, y = xyDataSeries.GetDataLabelPositionAtIndex(Index); @@ -81,7 +90,7 @@ internal override void UpdateDataLabels() dataLabel.Item = Item; dataLabel.Label = LabelContent ?? string.Empty; - LabelPositionPoint = InVisibleRange && !IsEmpty ? CartesianDataLabelSettings.CalculateDataLabelPoint(series, this, new PointF((float)DataLabelXPosition, (float)DataLabelYPosition), dataLabelSettings.LabelStyle) : new PointF(float.NaN, float.NaN); + LabelPositionPoint = InVisibleRange && !IsZero ? CartesianDataLabelSettings.CalculateDataLabelPoint(series, this, new PointF((float)DataLabelXPosition, (float)DataLabelYPosition), dataLabelSettings.LabelStyle) : new PointF(float.NaN, float.NaN); dataLabel.XPosition = LabelPositionPoint.X; dataLabel.YPosition = LabelPositionPoint.Y; diff --git a/maui/src/Charts/Segment/ChartSegment.cs b/maui/src/Charts/Segment/ChartSegment.cs index da55b357..b2b24b63 100644 --- a/maui/src/Charts/Segment/ChartSegment.cs +++ b/maui/src/Charts/Segment/ChartSegment.cs @@ -203,7 +203,7 @@ internal bool HasStroke internal PointF LabelPositionPoint { get; set; } - internal bool IsEmpty { get; set; } + internal bool IsZero { get; set; } internal SeriesView? SeriesView { get; set; } diff --git a/maui/src/Charts/Segment/FastLineSegment.cs b/maui/src/Charts/Segment/FastLineSegment.cs index 4274f728..63c7cced 100644 --- a/maui/src/Charts/Segment/FastLineSegment.cs +++ b/maui/src/Charts/Segment/FastLineSegment.cs @@ -541,7 +541,7 @@ internal override void OnDataLabelLayout() dataLabel.Item = xyDataSeries.ActualData?[i]; dataLabel.Label = LabelContent; - if (!InVisibleRange || IsEmpty) + if (!InVisibleRange || IsZero) { LabelPositionPoint = new PointF(float.NaN, float.NaN); } diff --git a/maui/src/Charts/Segment/HiLoOpenCloseSegment.cs b/maui/src/Charts/Segment/HiLoOpenCloseSegment.cs index 1ed694a6..33448550 100644 --- a/maui/src/Charts/Segment/HiLoOpenCloseSegment.cs +++ b/maui/src/Charts/Segment/HiLoOpenCloseSegment.cs @@ -165,7 +165,7 @@ void UpdateDataLabels(PointF highPoint, PointF lowPoint, PointF openPoint, Point void CalculateDataLabelPositions(double xValue, double high, double low, double open, double close, FinancialSeriesBase series) { - IsEmpty = double.IsNaN(high) && double.IsNaN(low); + IsZero = double.IsNaN(high) && double.IsNaN(low); InVisibleRange = series.IsDataInVisibleRange(xValue, high) && series.IsDataInVisibleRange(xValue, low); PointF highPoint = GetDataLabelPosition(xValue, high, series); PointF lowPoint = GetDataLabelPosition(xValue, low, series); diff --git a/maui/src/Charts/Segment/PieSegment.cs b/maui/src/Charts/Segment/PieSegment.cs index 2e207ce5..fab4c7d2 100644 --- a/maui/src/Charts/Segment/PieSegment.cs +++ b/maui/src/Charts/Segment/PieSegment.cs @@ -213,7 +213,7 @@ internal override void OnDataLabelLayout() } var dataLabelSettings = pieSeries.DataLabelSettings; - IsEmpty = double.IsNaN(YValue) || YValue == 0; + IsZero = double.IsNaN(YValue) || YValue == 0; float segmentRadius = pieSeries.GetDataLabelRadius(); segmentRadius = Index == pieSeries.ExplodeIndex ? segmentRadius + (float)pieSeries.ExplodeRadius : segmentRadius; PointF center = pieSeries.Center; diff --git a/maui/src/Charts/Segment/RangeAreaSegment.cs b/maui/src/Charts/Segment/RangeAreaSegment.cs index de4f587e..e7e7964f 100644 --- a/maui/src/Charts/Segment/RangeAreaSegment.cs +++ b/maui/src/Charts/Segment/RangeAreaSegment.cs @@ -161,7 +161,7 @@ internal override void OnDataLabelLayout() dataLabel.Item = series.ActualData[i]; dataLabel.Label = LabelContent ?? string.Empty; - if (!InVisibleRange || IsEmpty) + if (!InVisibleRange || IsZero) { LabelPositionPoint = new PointF(float.NaN, float.NaN); } @@ -195,7 +195,7 @@ internal override void OnDataLabelLayout() dataLabel.Item = series.ActualData[i]; dataLabel.Label = LabelContent ?? string.Empty; - if (!InVisibleRange || IsEmpty) + if (!InVisibleRange || IsZero) { LabelPositionPoint = new PointF(float.NaN, float.NaN); } diff --git a/maui/src/Charts/Segment/RangeColumnSegment.cs b/maui/src/Charts/Segment/RangeColumnSegment.cs index 0d822e40..2cfeee8e 100644 --- a/maui/src/Charts/Segment/RangeColumnSegment.cs +++ b/maui/src/Charts/Segment/RangeColumnSegment.cs @@ -190,7 +190,7 @@ void LayoutSegment() void CalculateDataLabelsPosition(double xValue, double high, double low, RangeColumnSeries series) { - IsEmpty = double.IsNaN(high) && double.IsNaN(low); + IsZero = double.IsNaN(high) && double.IsNaN(low); InVisibleRange = series.IsDataInVisibleRange(xValue, high) && series.IsDataInVisibleRange(xValue, low); double x = xValue, x1 = xValue, y = series.GetDataLabelPositionAtIndex(Index, high), y1 = series.GetDataLabelPositionAtIndex(Index, low); series.CalculateDataPointPosition(Index, ref x, ref y); diff --git a/maui/src/Charts/Segment/SplineRangeAreaSegment.cs b/maui/src/Charts/Segment/SplineRangeAreaSegment.cs index 0cf3a3fe..ba212924 100644 --- a/maui/src/Charts/Segment/SplineRangeAreaSegment.cs +++ b/maui/src/Charts/Segment/SplineRangeAreaSegment.cs @@ -139,7 +139,7 @@ internal override void OnDataLabelLayout() dataLabel.Item = series.ActualData[i]; dataLabel.Label = LabelContent ?? string.Empty; - if (!InVisibleRange || IsEmpty) + if (!InVisibleRange || IsZero) { LabelPositionPoint = new PointF(float.NaN, float.NaN); } @@ -173,7 +173,7 @@ internal override void OnDataLabelLayout() dataLabel.Item = series.ActualData[i]; dataLabel.Label = LabelContent ?? string.Empty; - if (!InVisibleRange || IsEmpty) + if (!InVisibleRange || IsZero) { LabelPositionPoint = new PointF(float.NaN, float.NaN); } diff --git a/maui/src/Charts/Series/AreaSeries.cs b/maui/src/Charts/Series/AreaSeries.cs index 80053e51..0e49e8d8 100644 --- a/maui/src/Charts/Series/AreaSeries.cs +++ b/maui/src/Charts/Series/AreaSeries.cs @@ -79,7 +79,7 @@ public partial class AreaSeries : XYDataSeries, IDrawCustomLegendIcon, IMarkerDe #region Fields bool _needToAnimateMarker; - + internal override bool IsFillEmptyPoint { get { return false; } } #endregion #region Bindable Properties diff --git a/maui/src/Charts/Series/BubbleSeries.cs b/maui/src/Charts/Series/BubbleSeries.cs index 3fc556eb..a6ad20a8 100644 --- a/maui/src/Charts/Series/BubbleSeries.cs +++ b/maui/src/Charts/Series/BubbleSeries.cs @@ -591,6 +591,45 @@ internal override void OnBindingPathChanged() ScheduleUpdateChart(); } + internal override void ResetEmptyPointIndexes() + { + if (EmptyPointIndexes.Length != 0) + { + if (EmptyPointIndexes[0] != null) + { + foreach (var index in EmptyPointIndexes[0]) + { + if (YValues != null && YValues.Count != 0) + { + YValues[(int)index] = double.NaN; + } + } + } + + if (EmptyPointIndexes[1] != null) + { + foreach (var index in EmptyPointIndexes[1]) + { + if (_sizeValues != null && _sizeValues.Count != 0) + { + _sizeValues[(int)index] = double.NaN; + } + } + } + } + } + + internal override void ValidateYValues() + { + bool yValues = YValues.Any(value => double.IsNaN(value)); + bool values = _sizeValues.Any(value => double.IsNaN(value)); + + if ((yValues || values) && SeriesYValues != null) + { + ValidateDataPoints(SeriesYValues); + } + } + #endregion #region Private Methods diff --git a/maui/src/Charts/Series/CartesianSeries.cs b/maui/src/Charts/Series/CartesianSeries.cs index 7e5ac2e7..c3d9ca9c 100644 --- a/maui/src/Charts/Series/CartesianSeries.cs +++ b/maui/src/Charts/Series/CartesianSeries.cs @@ -20,6 +20,10 @@ public abstract class CartesianSeries : ChartSeries #region Internal Properties + internal EmptyPointSettings _emptyPointSettings; + + internal IList[] EmptyPointIndexes { get; set; } = []; + internal CartesianChartArea? ChartArea { get; set; } internal bool IsIndexed @@ -48,10 +52,32 @@ internal bool IsGrouped internal override ChartDataLabelSettings ChartDataLabelSettings => DataLabelSettings; + internal bool RequiredEmptyPointReset { get; set; } = false; + + internal virtual bool IsFillEmptyPoint { get { return true; } } + #endregion #region Bindable properties + /// + /// Identifies the bindable property. + /// + /// + /// Represents how to calculate value for empty point in the series. + /// + public static readonly BindableProperty EmptyPointModeProperty = + BindableProperty.Create(nameof(EmptyPointMode), typeof(EmptyPointMode), typeof(CartesianSeries), EmptyPointMode.None, BindingMode.Default, null, OnEmptyPointModeChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// Provides customization for the empty points. + /// + public static readonly BindableProperty EmptyPointSettingsProperty = + BindableProperty.Create(nameof(EmptyPointSettings), typeof(EmptyPointSettings), typeof(CartesianSeries), null, BindingMode.Default, null, propertyChanged: OnEmptyPointSettingsChanged); + /// /// Identifies the bindable property. /// @@ -553,6 +579,108 @@ public DataTemplate TrackballLabelTemplate set { SetValue(TrackballLabelTemplateProperty, value); } } + /// + /// Gets or sets a value that indicates to displays NaN data points for the series. + /// + /// It accepts values and its default value is . + /// Empty points are not supported for the Histogram and BoxAndWhisker series + /// + /// # [Xaml](#tab/tabid-13) + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// # [C#](#tab/tabid-14) + /// + /// *** + /// + public EmptyPointMode EmptyPointMode + { + get { return (EmptyPointMode)GetValue(EmptyPointModeProperty); } + set { SetValue(EmptyPointModeProperty, value); } + } + + /// + /// Gets or sets the configuration for how empty or missing data points are handled and displayed within a chart series. + /// + /// It accepts values. + /// EmptyPointSettings is not supported for all area-related series, as well as for FastChart and ErrorBarSeries. + /// + /// # [Xaml](#tab/tabid-15) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// + /// # [C#](#tab/tabid-16) + /// + /// + /// *** + /// + public EmptyPointSettings EmptyPointSettings + { + get { return (EmptyPointSettings)GetValue(EmptyPointSettingsProperty); } + set { SetValue(EmptyPointSettingsProperty, value); } + } + /// /// Gets the actual XAxis value. /// @@ -611,6 +739,7 @@ internal set public CartesianSeries() { DataLabelSettings = new CartesianDataLabelSettings(); + _emptyPointSettings = new EmptyPointSettings(); } #endregion @@ -661,6 +790,17 @@ public CartesianSeries() #region Protected Methods + /// + protected override void OnParentSet() + { + base.OnParentSet(); + + if (EmptyPointSettings != null) + { + EmptyPointSettings.Parent = Parent; + } + } + /// /// protected override void OnBindingContextChanged() @@ -671,6 +811,11 @@ protected override void OnBindingContextChanged() { SetInheritedBindingContext(DataLabelSettings, BindingContext); } + + if (_emptyPointSettings != null) + { + SetInheritedBindingContext(_emptyPointSettings, BindingContext); + } } #endregion @@ -918,6 +1063,7 @@ internal override void OnDataSourceChanged(object oldValue, object newValue) { ResetAutoScroll(); InvalidateSideBySideSeries(); + RequiredEmptyPointReset = true; base.OnDataSourceChanged(oldValue, newValue); } @@ -927,6 +1073,24 @@ internal override void OnDataSource_CollectionChanged(object? sender, NotifyColl base.OnDataSource_CollectionChanged(sender, e); } + internal override void AddDataPoint(object data, int index, NotifyCollectionChangedEventArgs e) + { + RequiredEmptyPointReset = true; + base.AddDataPoint(data, index, e); + } + + internal override void RemoveData(int index, NotifyCollectionChangedEventArgs e) + { + RequiredEmptyPointReset = true; + base.RemoveData(index, e); + } + + internal override void OnBindingPathChanged() + { + RequiredEmptyPointReset = true; + base.OnBindingPathChanged(); + } + internal void ResetAutoScroll() { if (ActualXAxis != null) @@ -1597,12 +1761,230 @@ internal double[] GetCardinalSpline(List xValues, IList yValues) return yCoeff; } + internal override void UpdateEmptyPointSettings() + { + if (!IsFillEmptyPoint && EmptyPointMode == EmptyPointMode.None) + { + return; + } + + // Early return if no empty points or segments + if (EmptyPointIndexes == null || _segments.Count == 0) + { + return; + } + + // Cache the empty point settings for performance + var fill = _emptyPointSettings?.Fill; + var stroke = _emptyPointSettings?.Stroke; + var strokeWidth = _emptyPointSettings?.StrokeWidth ?? 0; + + // Apply settings to all empty points + foreach (var indexCollection in EmptyPointIndexes) + { + if (indexCollection == null) + { + continue; + } + + foreach (var index in indexCollection) + { + // Bounds check to prevent index out of range exceptions + if (index < 0 || index >= _segments.Count) + { + continue; + } + + if (_segments[index] is CartesianSegment segment) + { + segment.Fill = fill; + segment.Stroke = stroke; + segment.StrokeWidth = strokeWidth; + + // Mark segment as empty if it's a average or zero mode, for custom drawing. + segment.IsZero = true; + } + } + } + } + + internal void ValidateDataPoints(params IList[] yValues) + { + if (EmptyPointIndexes == null || EmptyPointIndexes.Length == 0) + { + EmptyPointIndexes = new List[yValues.Length]; + } + + for (int i = 0; i < yValues.Length; i++) + { + var values = yValues[i]; + + if (EmptyPointIndexes != null && EmptyPointIndexes[i] == null) + { + EmptyPointIndexes[i] = []; + } + + if (values.Count != 0) + { + switch (EmptyPointMode) + { + case EmptyPointMode.Zero: + HandleZeroMode(values, i); + break; + + case EmptyPointMode.Average: + HandleAverageMode(values, i); + break; + + default: + break; + } + } + + yValues[i] = values; + } + } + + internal virtual void ResetEmptyPointIndexes() + { + } + + internal virtual void ValidateYValues() + { + } + #endregion #endregion #region Private Methods + void HandleZeroMode(IList values, int emptyPointIndex) + { + for (int i = 0; i < values.Count; i++) + { + if (double.IsNaN(values[i])) + { + values[i] = 0; + if (!EmptyPointIndexes[emptyPointIndex].Contains(i)) + { + EmptyPointIndexes[emptyPointIndex].Add(i); + } + } + } + } + + void HandleAverageMode(IList values, int emptyPointIndex) + { + int j = 0; + //Single Data point + if (values.Count == 1 && double.IsNaN(values[j])) + { + values[j] = 0; + EmptyPointIndexes[emptyPointIndex].Add(0); + return; + } + + //First data point + if (double.IsNaN(values[j])) + { + values[j] = (0 + (double.IsNaN(values[j + 1]) ? 0 : values[j + 1])) / 2; + if (!EmptyPointIndexes[emptyPointIndex].Contains(j)) + { + EmptyPointIndexes[emptyPointIndex].Add(0); + } + } + + //Middle data points + for (; j < values.Count - 1; j++) + { + if (double.IsNaN(values[j])) + { + values[j] = (values[j - 1] + (double.IsNaN(values[j + 1]) ? 0 : values[j + 1])) / 2; + if (!EmptyPointIndexes[emptyPointIndex].Contains(j)) + { + EmptyPointIndexes[emptyPointIndex].Add(j); + } + } + } + + //Last data point + if (double.IsNaN(values[j])) + { + values[j] = values[j - 1] / 2; + if (!EmptyPointIndexes[emptyPointIndex].Contains(j)) + { + EmptyPointIndexes[emptyPointIndex].Add(j); + } + } + } + + static void OnEmptyPointSettingsChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is CartesianSeries series) + { + series.OnEmptyPointSettingsChanged(oldValue, newValue); + } + } + + void OnEmptyPointSettingsChanged(object oldValue, object newValue) + { + // Clean up old settings + if (oldValue is EmptyPointSettings oldSettings) + { + oldSettings.PropertyChanged -= EmptyPointSettings_PropertyChanged; + SetInheritedBindingContext(oldSettings, null); + oldSettings.Parent = null; + } + + // Initialize new settings + EmptyPointSettings settingsToUse; + + if (newValue is EmptyPointSettings newSettings) + { + settingsToUse = newSettings; + _emptyPointSettings = newSettings; + } + else + { + settingsToUse = new EmptyPointSettings(); + _emptyPointSettings = settingsToUse; + } + + // Configure the settings to use + settingsToUse.PropertyChanged += EmptyPointSettings_PropertyChanged; + if (Parent != null) + { + settingsToUse.Parent = Parent; + } + + UpdateEmptyPointSettings(); + + SetInheritedBindingContext(settingsToUse, BindingContext); + + InValidateViews(); + } + + void EmptyPointSettings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + UpdateEmptyPointSettings(); + InValidateViews(); + } + + static void OnEmptyPointModeChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is CartesianSeries series) + { + series.SegmentsCreated = false; + if (series.SeriesYValues != null) + { + series.RequiredEmptyPointReset = true; + } + + series.ScheduleUpdateChart(); + } + } + void GeneratePointInfos(List nearestDataPoints, List pointInfos) { var xValues = GetXValues(); diff --git a/maui/src/Charts/Series/ChartSeries.cs b/maui/src/Charts/Series/ChartSeries.cs index 4101dd2a..1589437e 100644 --- a/maui/src/Charts/Series/ChartSeries.cs +++ b/maui/src/Charts/Series/ChartSeries.cs @@ -1160,6 +1160,11 @@ public ChartSeries() Rect IDatapointSelectionDependent.AreaBounds => AreaBounds; void IDatapointSelectionDependent.Invalidate() + { + InValidateViews(); + } + + internal void InValidateViews() { InvalidateSeries(); @@ -1332,6 +1337,10 @@ protected override void OnParentSet() #region Internal methods + internal virtual void UpdateEmptyPointSettings() + { + } + internal virtual Brush? GetSegmentFillColor(int index) { var segment = _segments[index]; @@ -1381,7 +1390,7 @@ internal virtual void DrawDataLabels(ICanvas canvas) foreach (ChartSegment segment in _segments) { - if (!segment.InVisibleRange || segment.IsEmpty) + if (!segment.InVisibleRange || segment.IsZero) { continue; } diff --git a/maui/src/Charts/Series/ChartSeriesPartial.cs b/maui/src/Charts/Series/ChartSeriesPartial.cs index d6bce711..72f08184 100644 --- a/maui/src/Charts/Series/ChartSeriesPartial.cs +++ b/maui/src/Charts/Series/ChartSeriesPartial.cs @@ -81,6 +81,13 @@ internal ChartValueType XValueType #region Internal Methods +#pragma warning disable IDE0060 // Remove unused parameter + internal virtual void AddDataPoint(object data, int index, NotifyCollectionChangedEventArgs e) +#pragma warning restore IDE0060 // Remove unused parameter + { + SetIndividualPoint(data, index, false); + } + internal virtual void LegendItemToggled(LegendItem chartLegendItem) { } @@ -1310,13 +1317,6 @@ internal static ChartValueType GetDataType(FastReflection fastReflection, IEnume return parentObj; } -#pragma warning disable IDE0060 // Remove unused parameter - void AddDataPoint(int index, object data, NotifyCollectionChangedEventArgs e) -#pragma warning restore IDE0060 // Remove unused parameter - { - SetIndividualPoint(index, data, false); - } - void ResetDataPoint() { ResetData(); diff --git a/maui/src/Charts/Series/ErrorBarSeries.cs b/maui/src/Charts/Series/ErrorBarSeries.cs index edf709b4..e108c23d 100644 --- a/maui/src/Charts/Series/ErrorBarSeries.cs +++ b/maui/src/Charts/Series/ErrorBarSeries.cs @@ -79,6 +79,8 @@ public partial class ErrorBarSeries : XYDataSeries, IDrawCustomLegendIcon internal IList VerticalErrorValues { get; set; } + internal override bool IsFillEmptyPoint { get { return false; } } + #endregion #region Bindable Properties diff --git a/maui/src/Charts/Series/FastLineSeries.cs b/maui/src/Charts/Series/FastLineSeries.cs index c1ff0b03..990565dd 100644 --- a/maui/src/Charts/Series/FastLineSeries.cs +++ b/maui/src/Charts/Series/FastLineSeries.cs @@ -79,6 +79,8 @@ public partial class FastLineSeries : XYDataSeries, IDrawCustomLegendIcon internal double ToleranceCoefficient { get; set; } + internal override bool IsFillEmptyPoint { get { return false; } } + #endregion #region Bindable Properties diff --git a/maui/src/Charts/Series/FinancialSeriesBase.cs b/maui/src/Charts/Series/FinancialSeriesBase.cs index 2046bda6..d42213ad 100644 --- a/maui/src/Charts/Series/FinancialSeriesBase.cs +++ b/maui/src/Charts/Series/FinancialSeriesBase.cs @@ -627,6 +627,69 @@ internal override void CalculateDataPointPosition(int index, ref double x, ref d } } + internal override void ResetEmptyPointIndexes() + { + if (EmptyPointIndexes.Length != 0) + { + if (EmptyPointIndexes[0] != null) + { + foreach (var index in EmptyPointIndexes[0]) + { + if (HighValues != null && HighValues.Count != 0) + { + HighValues[(int)index] = double.NaN; + } + } + } + + if (EmptyPointIndexes[1] != null) + { + foreach (var index in EmptyPointIndexes[1]) + { + if (LowValues != null && LowValues.Count != 0) + { + LowValues[(int)index] = double.NaN; + } + } + } + + if (EmptyPointIndexes[2] != null) + { + foreach (var index in EmptyPointIndexes[2]) + { + if (OpenValues != null && OpenValues.Count != 0) + { + OpenValues[(int)index] = double.NaN; + } + } + } + + if (EmptyPointIndexes[3] != null) + { + foreach (var index in EmptyPointIndexes[3]) + { + if (CloseValues != null && CloseValues.Count != 0) + { + CloseValues[(int)index] = double.NaN; + } + } + } + } + } + + internal override void ValidateYValues() + { + bool highValues = HighValues.Any(value => double.IsNaN(value)); + bool lowValues = LowValues.Any(value => double.IsNaN(value)); + bool openValues = OpenValues.Any(value => double.IsNaN(value)); + bool closeValues = CloseValues.Any(value => double.IsNaN(value)); + + if ((highValues || lowValues || openValues || closeValues) && SeriesYValues != null) + { + ValidateDataPoints(SeriesYValues); + } + } + internal override void DrawDataLabels(ICanvas canvas) { var dataLabelSettings = ChartDataLabelSettings; @@ -639,7 +702,7 @@ internal override void DrawDataLabels(ICanvas canvas) foreach (HiLoOpenCloseSegment dataLabel in _segments.Cast()) { - if (!dataLabel.InVisibleRange || dataLabel.IsEmpty) + if (!dataLabel.InVisibleRange || dataLabel.IsZero) { continue; } diff --git a/maui/src/Charts/Series/HiLoOpenCloseSeries.cs b/maui/src/Charts/Series/HiLoOpenCloseSeries.cs index 9efff754..43526887 100644 --- a/maui/src/Charts/Series/HiLoOpenCloseSeries.cs +++ b/maui/src/Charts/Series/HiLoOpenCloseSeries.cs @@ -320,7 +320,7 @@ internal virtual void CreateSegment(SeriesView seriesView, double[] values, bool X = xPosition, Y = yPosition, Index = index, - Text = yValue.ToString(" #.##") + "/" + lowValue.ToString(" #.##") + "/" + openValue.ToString(" #.##") + "/" + closeValue.ToString(" #.##"), + Text = (yValue == 0 ? yValue.ToString(" 0.##") : yValue.ToString(" #.##")) + "/" + (lowValue == 0 ? lowValue.ToString(" 0.##") : lowValue.ToString(" #.##")) + "/" + (openValue == 0 ? openValue.ToString(" 0.##") : openValue.ToString(" #.##")) + "/" + (closeValue == 0 ? closeValue.ToString(" 0.##") : closeValue.ToString(" #.##")), Margin = tooltipBehavior.Margin, TextColor = tooltipBehavior.TextColor, FontFamily = tooltipBehavior.FontFamily, diff --git a/maui/src/Charts/Series/RangeAreaSeries.cs b/maui/src/Charts/Series/RangeAreaSeries.cs index 90bc3b77..d0e26674 100644 --- a/maui/src/Charts/Series/RangeAreaSeries.cs +++ b/maui/src/Charts/Series/RangeAreaSeries.cs @@ -485,7 +485,7 @@ internal override void GenerateSegments(SeriesView seriesView) } else if (!double.IsNaN(lowValueContent)) { - tooltipInfo.Text += "/" + lowValueContent.ToString("#.##"); + tooltipInfo.Text += "/" + (lowValueContent == 0 ? lowValueContent.ToString("0.##") : lowValueContent.ToString("#.##")); } return tooltipInfo; diff --git a/maui/src/Charts/Series/RangeColumnSeries.cs b/maui/src/Charts/Series/RangeColumnSeries.cs index 02598050..e5058237 100644 --- a/maui/src/Charts/Series/RangeColumnSeries.cs +++ b/maui/src/Charts/Series/RangeColumnSeries.cs @@ -392,7 +392,7 @@ internal override double GetActualSpacing() if (!double.IsNaN(lowValueContent)) { - tooltipInfo.Text += "/" + lowValueContent.ToString("#.##"); + tooltipInfo.Text += "/" + (lowValueContent == 0 ? lowValueContent.ToString("0.##") : lowValueContent.ToString("#.##")); } return tooltipInfo; @@ -465,7 +465,7 @@ internal override void DrawDataLabels(ICanvas canvas) ChartDataLabelStyle labelStyle = dataLabelSettings.LabelStyle; foreach (RangeColumnSegment dataLabel in _segments) { - if (!dataLabel.InVisibleRange || dataLabel.IsEmpty) + if (!dataLabel.InVisibleRange || dataLabel.IsZero) { continue; } diff --git a/maui/src/Charts/Series/RangeSeriesBase .cs b/maui/src/Charts/Series/RangeSeriesBase .cs index 50feae90..9933f103 100644 --- a/maui/src/Charts/Series/RangeSeriesBase .cs +++ b/maui/src/Charts/Series/RangeSeriesBase .cs @@ -384,6 +384,45 @@ internal override void OnDataSourceChanged(object oldValue, object newValue) base.OnDataSourceChanged(oldValue, newValue); } + internal override void ResetEmptyPointIndexes() + { + if (EmptyPointIndexes.Length != 0) + { + if (EmptyPointIndexes[0] != null) + { + foreach (var index in EmptyPointIndexes[0]) + { + if (HighValues != null && HighValues.Count != 0) + { + HighValues[(int)index] = double.NaN; + } + } + } + + if (EmptyPointIndexes[1] != null) + { + foreach (var index in EmptyPointIndexes[1]) + { + if (LowValues != null && LowValues.Count != 0) + { + LowValues[(int)index] = double.NaN; + } + } + } + } + } + + internal override void ValidateYValues() + { + bool highValues = HighValues.Any(value => double.IsNaN(value)); + bool lowValues = LowValues.Any(value => double.IsNaN(value)); + + if ((highValues || lowValues) && SeriesYValues != null) + { + ValidateDataPoints(SeriesYValues); + } + } + internal override void OnBindingPathChanged() { ResetData(); diff --git a/maui/src/Charts/Series/SplineRangeAreaSeries.cs b/maui/src/Charts/Series/SplineRangeAreaSeries.cs index 85da04ac..4557089e 100644 --- a/maui/src/Charts/Series/SplineRangeAreaSeries.cs +++ b/maui/src/Charts/Series/SplineRangeAreaSeries.cs @@ -464,7 +464,7 @@ protected override void OnBindingContextChanged() } else if (!double.IsNaN(lowValueContent)) { - tooltipInfo.Text += "/" + lowValueContent.ToString("#.##"); + tooltipInfo.Text += "/" + (lowValueContent == 0 ? lowValueContent.ToString("0.##") : lowValueContent.ToString("#.##")); } return tooltipInfo; diff --git a/maui/src/Charts/Series/WaterfallSeries.cs b/maui/src/Charts/Series/WaterfallSeries.cs index 7112abd9..430c11b9 100644 --- a/maui/src/Charts/Series/WaterfallSeries.cs +++ b/maui/src/Charts/Series/WaterfallSeries.cs @@ -831,7 +831,7 @@ internal override void DrawDataLabels(ICanvas canvas) foreach (ChartSegment dataLabel in _segments) { - if (!dataLabel.IsEmpty) + if (!dataLabel.IsZero) { UpdateDataLabelAppearance(canvas, dataLabel, dataLabelSettings, labelStyle); } diff --git a/maui/src/Charts/Series/XYDataSeries.cs b/maui/src/Charts/Series/XYDataSeries.cs index edc37a03..c663857d 100644 --- a/maui/src/Charts/Series/XYDataSeries.cs +++ b/maui/src/Charts/Series/XYDataSeries.cs @@ -185,6 +185,37 @@ internal virtual double GetDataLabelPositionAtIndex(int index) return YValues == null ? 0f : YValues[index]; } + internal override void ValidateYValues() + { + foreach (var yValue in YValues) + { + if (double.IsNaN(yValue)) + { + ValidateDataPoints(YValues); + break; + } + } + } + + internal override void ResetEmptyPointIndexes() + { + if (EmptyPointIndexes.Length != 0) + { + foreach (var item in EmptyPointIndexes) + { + if (item != null) + { + foreach (var index in item) + { + if (YValues != null && YValues.Count != 0) + { + YValues[(int)index] = double.NaN; + } + } + } + } + } + } #endregion #region Private Methods diff --git a/maui/src/Charts/Styles/EmptyPointSettings.cs b/maui/src/Charts/Styles/EmptyPointSettings.cs new file mode 100644 index 00000000..eae7a42e --- /dev/null +++ b/maui/src/Charts/Styles/EmptyPointSettings.cs @@ -0,0 +1,293 @@ +namespace Syncfusion.Maui.Toolkit.Charts +{ + + /// + /// Used to customize the empty points which are NaN data points. + /// + /// + /// By customizing these empty points, the positions of the data points are highlighted, thereby improving data visualization. + /// + /// EmptyPointSettings class provides properties to customize these empty points by modifying attributes such as , , and . + /// + /// To customize empty points, create an instance of , configure it as needed, and then add it to the series. + /// + /// EmptyPointSettings is not supported for all area-related series, as well as for FastChart. + /// # [MainPage.xaml](#tab/tabid-1) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// + /// # [MainPage.xaml.cs](#tab/tabid-2) + /// + /// + /// *** + /// + public class EmptyPointSettings : Element + { + #region Bindable Properties + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for the bindable property determines the fill color of the empty point. + /// + public static readonly BindableProperty FillProperty = + BindableProperty.Create(nameof(Fill), typeof(Brush), typeof(EmptyPointSettings), null, BindingMode.Default, null, OnFillChanged, null, defaultValueCreator: FillDefaultValueCreator); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for the bindable property determines the stroke color of the empty point. + /// + public static readonly BindableProperty StrokeProperty = + BindableProperty.Create(nameof(Stroke), typeof(Brush), typeof(EmptyPointSettings), null, BindingMode.Default, null, OnStrokeChanged, null, defaultValueCreator: StrokeDefaultValueCreator); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for the bindable property determines the stroke width of the empty point. + /// + public static readonly BindableProperty StrokeWidthProperty = + BindableProperty.Create(nameof(StrokeWidth), typeof(double), typeof(EmptyPointSettings), 1.0); + + #endregion + + #region Public Properties + + /// + /// Gets or sets the value to fill empty point. + /// + /// It accepts a value and its default value is FF4F4F. + /// + /// # [Xaml](#tab/tabid-3) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// # [C#](#tab/tabid-4) + /// + /// *** + /// + public Brush Fill + { + get { return (Brush)GetValue(FillProperty); } + set { SetValue(FillProperty, value); } + } + + /// + /// Gets or sets the value for empty point stroke. + /// + /// It accepts values and its default value is Transparent. + /// EmptyPointSettings of stroke is not supported for Waterfall series. + /// + /// # [Xaml](#tab/tabid-5) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// # [C#](#tab/tabid-6) + /// + /// *** + /// + public Brush Stroke + { + get { return (Brush)GetValue(StrokeProperty); } + set { SetValue(StrokeProperty, value); } + } + + /// + /// Gets or sets the value for empty point stroke width. + /// + /// The value needs to be greater than zero. + /// EmptyPointSettings of StrokeWidth is not supported for Waterfall series. + /// + /// It accepts double values and its default value is 1. + /// + /// # [Xaml](#tab/tabid-7) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// # [C#](#tab/tabid-8) + /// + /// *** + /// + public double StrokeWidth + { + get { return (double)GetValue(StrokeWidthProperty); } + set { SetValue(StrokeWidthProperty, value); } + } + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the + /// + public EmptyPointSettings() + { + } + + #endregion + + #region Methods + + #region private methods + + static void OnFillChanged(BindableObject bindable, object oldValue, object newValue) + { + } + + static void OnStrokeChanged(BindableObject bindable, object oldValue, object newValue) + { + } + + static object StrokeDefaultValueCreator(BindableObject bindable) + { + return new SolidColorBrush(Colors.Transparent); + } + static object FillDefaultValueCreator(BindableObject bindable) + { + return new SolidColorBrush(Color.FromArgb("FF4E4E")); + } + + #endregion + + #endregion + } +} \ No newline at end of file diff --git a/maui/src/Charts/Utils/Enum.cs b/maui/src/Charts/Utils/Enum.cs index 2833e6a6..1d831598 100644 --- a/maui/src/Charts/Utils/Enum.cs +++ b/maui/src/Charts/Utils/Enum.cs @@ -1,5 +1,29 @@ namespace Syncfusion.Maui.Toolkit.Charts { + /// + /// Defines modes for handling empty data points in a chart series. + /// + public enum EmptyPointMode + { + /// + /// Leaves empty points unmodified, maintaining them as NaN (Not a Number). + /// Suitable for explicitly skipping or not displaying missing data points. + /// + None, + + /// + /// Replaces empty points with a value of zero. + /// Useful for treating missing data points as zero values, maintaining continuity in series where a missing data point logically represents zero impact. + /// + Zero, + + /// + /// Calculates empty points as the average of the surrounding data points. + /// Ideal for approximating missing data by averaging neighboring values, facilitating smooth transitions in data where interim values are uncertain. + /// + Average, + } + /// /// /// diff --git a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/EmptyPointSettingUnitTest.cs b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/EmptyPointSettingUnitTest.cs new file mode 100644 index 00000000..ee5758a8 --- /dev/null +++ b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/EmptyPointSettingUnitTest.cs @@ -0,0 +1,219 @@ +using Syncfusion.Maui.Toolkit.Charts; + +namespace Syncfusion.Maui.Toolkit.UnitTest +{ + public class EmptyPointSettingUnitTest + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IsEmpty_SetValue_ReturnsExpectedValue(bool expectedIsEmpty) + { + ColumnSegment columnSegment = new ColumnSegment + { + IsZero = expectedIsEmpty + }; + Assert.Equal(expectedIsEmpty, columnSegment.IsZero); + } + + [Fact] + public void ReValidateYValues_SetsValuesToNaN_Correctly() + { + var columnSeries = new ColumnSeries(); + columnSeries.YValues = new List { 1, 2, 3, 4, 5 }; + IList[] emptyPointIndex = new IList[] + { + new List { 1, 3 }, + new List() + }; + columnSeries.EmptyPointIndexes = emptyPointIndex; + columnSeries.ResetEmptyPointIndexes(); + Assert.Equal(new List { 1.0, double.NaN, 3.0, double.NaN, 5.0 }, columnSeries.YValues); + } + + [Fact] + public void EmptyPointIndexes_ShouldBeInitializedByDefault() + { + var series = new ColumnSeries(); + var emptyPointIndexes = series.EmptyPointIndexes; + Assert.NotNull(emptyPointIndexes); + } + + [Fact] + public void EmptyPointIndexes_CanBeSetAndRetrieved() + { + var series = new ColumnSeries(); + var indexes = new List[] + { + new List { 2, 4 }, + new List {3 }, + new List() + }; + + series.EmptyPointIndexes = indexes; + var retrievedIndexes = series.EmptyPointIndexes; + Assert.NotNull(retrievedIndexes); + Assert.Equal(3, retrievedIndexes.Length); + Assert.Equal(new List { 2, 4 }, retrievedIndexes[0]); + Assert.Equal(new List { 3 }, retrievedIndexes[1]); + Assert.Empty(retrievedIndexes[2]); + } + + [Fact] + public void EmptyPointSettings_DefaultValue_ShouldBeInitialized() + { + var series = new ColumnSeries(); + var settings = series.EmptyPointSettings; + var point = series._emptyPointSettings; + Assert.Null(settings); + Assert.NotNull(point); + } + + [Fact] + public void EmptyPointMode_DefaultValue_ShouldBeNone() + { + var series = new ColumnSeries(); + var mode = series.EmptyPointMode; + Assert.Equal(EmptyPointMode.None, mode); + } + + [Theory] + [InlineData(EmptyPointMode.Zero)] + [InlineData(EmptyPointMode.Average)] + [InlineData(EmptyPointMode.None)] + public void EmptyPointMode_SetAndRetrieveValue(EmptyPointMode emptyPointMode) + { + var series = new ColumnSeries(); + series.EmptyPointMode = emptyPointMode; + var mode = series.EmptyPointMode; + Assert.Equal(emptyPointMode, mode); + } + + [Fact] + public void EmptyPointSettings_CanBeSetAndRetrieved() + { + var series = new ColumnSeries(); + var emptyPointSettings = new EmptyPointSettings + { + Fill = new SolidColorBrush(Colors.Red), + Stroke = new SolidColorBrush(Colors.Blue), + StrokeWidth = 2 + }; + + series.EmptyPointSettings = emptyPointSettings; + var settings = series.EmptyPointSettings; + Assert.NotNull(settings); + Assert.Equal(emptyPointSettings, settings); + Assert.Equal(Colors.Red, ((SolidColorBrush)settings.Fill).Color); + Assert.Equal(Colors.Blue, ((SolidColorBrush)settings.Stroke).Color); + Assert.Equal(2, settings.StrokeWidth); + } + + [Fact] + public void ValidateYPoints_With10Points_EmptyPointMode_Average_ShouldReplaceNaNWithAverage() + { + var series = new ColumnSeries() { YValues = new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 } }; + series.EmptyPointMode = EmptyPointMode.Average; + series.ValidateYValues(); + Assert.Equal(new List { 1.0, 1.5, 2.0, 3.0, 4.25, 5.5, 6.0, 7.0, 8.5, 10.0 }, series.YValues); + } + + [Fact] + public void ValidateYPoints_With10Points_EmptyPointMode_Zero_ShouldReplaceNaNWithZero() + { + var series = new ColumnSeries + { + YValues = new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 }, + EmptyPointMode = EmptyPointMode.Zero + }; + series.ValidateYValues(); + Assert.Equal(new List { 1.0, 0, 2.0, 3.0, 0, 5.5, 6.0, 7.0, 0, 10.0 }, series.YValues); + } + + [Fact] + public void ValidateDataPoints_With10Points_EmptyPointMode_None_ShouldRemainUnchanged() + { + var series = new ColumnSeries(); + series.EmptyPointMode = EmptyPointMode.None; + var yValues = new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 }; + series.ValidateDataPoints(yValues); + Assert.Equal(new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 }, yValues); + } + + [Fact] + public void ValidateDataPoints_With10Points_EmptyPointMode_Zero_ShouldReplaceNaNWithZero() + { + var series = new ColumnSeries(); + series.EmptyPointMode = EmptyPointMode.Zero; + var yValues = new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 }; + series.ValidateDataPoints(yValues); + Assert.Equal(new List { 1.0, 0.0, 2.0, 3.0, 0.0, 5.5, 6.0, 7.0, 0.0, 10.0 }, yValues); + } + + [Fact] + public void ValidateDataPoints_With10Points_EmptyPointMode_Average_ShouldReplaceNaNWithAverage() + { + var series = new ColumnSeries(); + series.EmptyPointMode = EmptyPointMode.Average; + var yValues = new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 }; + series.ValidateDataPoints(yValues); + Assert.Equal(new List { 1.0, 1.5, 2.0, 3.0, 4.25, 5.5, 6.0, 7.0, 8.5, 10.0 }, yValues); + } + + [Fact] + public void FillProperty_DefaultValue_ShouldBeDefaultBrush() + { + var emptyPointSettings = new EmptyPointSettings(); + var defaultBrush = new SolidColorBrush(Color.FromArgb("FF4E4E")); + var defaultFillValue = emptyPointSettings.Fill; + Assert.Equal(defaultBrush.Color, ((SolidColorBrush)defaultFillValue).Color); + } + + [Fact] + public void FillProperty_SetValue_ShouldReturnSetValue() + { + var emptyPointSettings = new EmptyPointSettings(); + var expectedBrush = new SolidColorBrush(Colors.Red); + emptyPointSettings.Fill = expectedBrush; + var fillValue = emptyPointSettings.Fill; + Assert.Equal(expectedBrush, fillValue); + } + + [Fact] + public void StrokeProperty_DefaultValue_ShouldBeTransparent() + { + var emptyPointSettings = new EmptyPointSettings(); + var defaultStroke = new SolidColorBrush(Colors.Transparent); + var defaultStrokeValue = emptyPointSettings.Stroke; + Assert.Equal(defaultStroke.Color, ((SolidColorBrush)defaultStrokeValue).Color); + } + + [Fact] + public void StrokeProperty_SetValue_ShouldReturnSetValue() + { + var emptyPointSettings = new EmptyPointSettings(); + var expectedStrokeBrush = new SolidColorBrush(Colors.Blue); + emptyPointSettings.Stroke = expectedStrokeBrush; + var strokeValue = emptyPointSettings.Stroke; + Assert.Equal(expectedStrokeBrush.Color, ((SolidColorBrush)strokeValue).Color); + } + + [Fact] + public void StrokeWidthProperty_DefaultValue_ShouldBeOne() + { + var emptyPointSettings = new EmptyPointSettings(); + var defaultStrokeWidth = emptyPointSettings.StrokeWidth; + Assert.Equal(1.0, defaultStrokeWidth); + } + + [Fact] + public void StrokeWidthProperty_SetValue_ShouldReturnSetValue() + { + var emptyPointSettings = new EmptyPointSettings(); + var expectedStrokeWidth = 2.5; + emptyPointSettings.StrokeWidth = expectedStrokeWidth; + var strokeWidth = emptyPointSettings.StrokeWidth; + Assert.Equal(expectedStrokeWidth, strokeWidth); + } + } +} From 77c9aebc9a98d36f10bed92e356f1662e2c0752b Mon Sep 17 00:00:00 2001 From: Harsha Date: Wed, 12 Mar 2025 11:40:10 +0530 Subject: [PATCH 2/5] updated code for dynamic empty point values --- .../DataLabel/ChartDataLabelSettings.cs | 2 +- maui/src/Charts/Series/CartesianSeries.cs | 37 +++++++++++++++---- maui/src/Charts/Series/ChartSeriesPartial.cs | 4 +- maui/src/Charts/Styles/EmptyPointSettings.cs | 6 +-- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/maui/src/Charts/DataLabel/ChartDataLabelSettings.cs b/maui/src/Charts/DataLabel/ChartDataLabelSettings.cs index 3e54430b..88ad1484 100644 --- a/maui/src/Charts/DataLabel/ChartDataLabelSettings.cs +++ b/maui/src/Charts/DataLabel/ChartDataLabelSettings.cs @@ -360,7 +360,7 @@ internal string GetLabelContent(double value) } else { - labelContent = value.ToString("#.##"); + labelContent = value == 0 ? value.ToString("0.##") : value.ToString("#.##"); } return labelContent; diff --git a/maui/src/Charts/Series/CartesianSeries.cs b/maui/src/Charts/Series/CartesianSeries.cs index c3d9ca9c..d6ccb814 100644 --- a/maui/src/Charts/Series/CartesianSeries.cs +++ b/maui/src/Charts/Series/CartesianSeries.cs @@ -1063,8 +1063,15 @@ internal override void OnDataSourceChanged(object oldValue, object newValue) { ResetAutoScroll(); InvalidateSideBySideSeries(); - RequiredEmptyPointReset = true; - base.OnDataSourceChanged(oldValue, newValue); + foreach (var item in EmptyPointIndexes) + { + if (item != null) + + { + item.Clear(); + } + } + base.OnDataSourceChanged(oldValue, newValue); } internal override void OnDataSource_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -1073,16 +1080,32 @@ internal override void OnDataSource_CollectionChanged(object? sender, NotifyColl base.OnDataSource_CollectionChanged(sender, e); } - internal override void AddDataPoint(object data, int index, NotifyCollectionChangedEventArgs e) + internal override void AddDataPoint(int index, object data, NotifyCollectionChangedEventArgs e) { - RequiredEmptyPointReset = true; - base.AddDataPoint(data, index, e); + ResetEmptyPointIndexes(); + foreach (var item in EmptyPointIndexes) + { + if (item != null) + + { + item.Clear(); + } + } + base.AddDataPoint(index, data, e); } internal override void RemoveData(int index, NotifyCollectionChangedEventArgs e) { - RequiredEmptyPointReset = true; - base.RemoveData(index, e); + ResetEmptyPointIndexes(); + foreach (var item in EmptyPointIndexes) + { + if (item != null) + + { + item.Clear(); + } + } + base.RemoveData(index, e); } internal override void OnBindingPathChanged() diff --git a/maui/src/Charts/Series/ChartSeriesPartial.cs b/maui/src/Charts/Series/ChartSeriesPartial.cs index 72f08184..dd869f31 100644 --- a/maui/src/Charts/Series/ChartSeriesPartial.cs +++ b/maui/src/Charts/Series/ChartSeriesPartial.cs @@ -82,10 +82,10 @@ internal ChartValueType XValueType #region Internal Methods #pragma warning disable IDE0060 // Remove unused parameter - internal virtual void AddDataPoint(object data, int index, NotifyCollectionChangedEventArgs e) + internal virtual void AddDataPoint(int index, object data, NotifyCollectionChangedEventArgs e) #pragma warning restore IDE0060 // Remove unused parameter { - SetIndividualPoint(data, index, false); + SetIndividualPoint(index, data, false); } internal virtual void LegendItemToggled(LegendItem chartLegendItem) diff --git a/maui/src/Charts/Styles/EmptyPointSettings.cs b/maui/src/Charts/Styles/EmptyPointSettings.cs index eae7a42e..d4bbec49 100644 --- a/maui/src/Charts/Styles/EmptyPointSettings.cs +++ b/maui/src/Charts/Styles/EmptyPointSettings.cs @@ -11,7 +11,7 @@ /// /// To customize empty points, create an instance of , configure it as needed, and then add it to the series. /// - /// EmptyPointSettings is not supported for all area-related series, as well as for FastChart. + /// EmptyPointSettings is not supported for all area-related series, as well as for FastChart and ErrorBarSeries. /// # [MainPage.xaml](#tab/tabid-1) /// @@ -58,7 +58,7 @@ /// series.EmptyPointMode = EmptyPointMode.Zero; /// series.EmptyPointSettings = new EmptyPointSettings(); /// series.EmptyPointSettings.Fill = Colors.Orange; - /// series.MarEmptyPointSettingskerSettings.Stroke = Colors.Red; + /// series.EmptyPointSettings.Stroke = Colors.Red; /// series.EmptyPointSettings.StrokeWidth = 3; /// chart.Series.Add(series); /// ]]> @@ -187,7 +187,7 @@ public Brush Fill /// EmptyPointMode = EmptyPointMode.Average, /// }; /// series.EmptyPointSettings = new EmptyPointSettings(); - /// series.MarEmptyPointSettingskerSettings.Stroke = Colors.Red; + /// series.EmptyPointSettings.Stroke = Colors.Red; /// series.EmptyPointSettings.StrokeWidth = 3; /// chart.Series.Add(series); /// From 9406379a9236ab5e32721522d7c7a5555be75b92 Mon Sep 17 00:00:00 2001 From: Harsha Date: Wed, 12 Mar 2025 16:03:31 +0530 Subject: [PATCH 3/5] added test case for data label --- maui/src/Charts/Series/FastLineSeries.cs | 1 + .../Chart/Features/DataLabelUnitTests.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/maui/src/Charts/Series/FastLineSeries.cs b/maui/src/Charts/Series/FastLineSeries.cs index 990565dd..560b3681 100644 --- a/maui/src/Charts/Series/FastLineSeries.cs +++ b/maui/src/Charts/Series/FastLineSeries.cs @@ -16,6 +16,7 @@ namespace Syncfusion.Maui.Toolkit.Charts /// Data Label - Data labels are used to display values related to a chart segment. To render the data labels, you need to set the property as true in class. To customize the chart data labels alignment, placement, and label styles, you need to create an instance of and set to the property. /// Animation - To animate the series, set True to the property. /// LegendIcon - To customize the legend icon using the property. + /// The FastLineSeries does not support empty points. /// /// /// # [Xaml](#tab/tabid-1) diff --git a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/DataLabelUnitTests.cs b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/DataLabelUnitTests.cs index 0f8b8391..a197e1a6 100644 --- a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/DataLabelUnitTests.cs +++ b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/DataLabelUnitTests.cs @@ -601,6 +601,24 @@ public void GetLabelContent_VariousInputs_ReturnsExpectedContent(double value, s Assert.Equal(expectedContent, result); } + [Theory] + [InlineData(double.NaN, "", "")] + [InlineData(0, "0", "")] + [InlineData(0, "000", "000")] + [InlineData(123.456, "123", "#")] + [InlineData(123.456, "123.46", "")] + [InlineData(123.456, "123.456", "#.###")] + [InlineData(123.0, "123", "0")] + public void GetLabelContent_VariousInput_ReturnsExpectedContent(double value, string expectedContent, string format) + { + var cartesianDataLabelSettings = new CartesianDataLabelSettings + { + LabelStyle = new ChartDataLabelStyle { LabelFormat = format } + }; + var result = cartesianDataLabelSettings.GetLabelContent(value); + Assert.Equal(expectedContent, result); + } + [Fact] public void GetDefaultTextColor_ReturnsExpectedColor() { From a47db9779c70e1e6a0a14b03ba906d4118ac64c2 Mon Sep 17 00:00:00 2001 From: Harsha Date: Wed, 12 Mar 2025 18:16:46 +0530 Subject: [PATCH 4/5] cleared massage warnings --- maui/src/Charts/Series/CartesianSeries.cs | 6 +- maui/src/Charts/Series/ChartSeries.cs | 100 ++++++++---------- maui/src/Charts/Styles/ChartMarkerSettings.cs | 2 +- maui/src/Charts/Utils/ChartUtils.cs | 6 +- .../Features/EmptyPointSettingUnitTest.cs | 71 ++++++++----- 5 files changed, 93 insertions(+), 92 deletions(-) diff --git a/maui/src/Charts/Series/CartesianSeries.cs b/maui/src/Charts/Series/CartesianSeries.cs index d6ccb814..e94ffb75 100644 --- a/maui/src/Charts/Series/CartesianSeries.cs +++ b/maui/src/Charts/Series/CartesianSeries.cs @@ -1355,7 +1355,7 @@ internal bool IsDataInVisibleRange(double xValue, double yValue) FontSize = tooltipBehavior.FontSize, FontAttributes = tooltipBehavior.FontAttributes, Background = tooltipBehavior.Background, - Text = yValue.ToString("#.##"), + Text = yValue == 0 ? yValue.ToString("0.##") : yValue.ToString("#.##"), Item = dataPoint }; return tooltipInfo; @@ -1499,9 +1499,9 @@ internal List FindNearestChartPoints(float pointX, float pointY) if (xValue <= xEnd && xValue >= xStart && xValue < dataCount && xValue >= 0) { - var dataPoint = new object(); + object? dataPoint; - if (isGrouped) + if (isGrouped) { dataPoint = GroupedActualData?[(int)xValue]; } diff --git a/maui/src/Charts/Series/ChartSeries.cs b/maui/src/Charts/Series/ChartSeries.cs index 1589437e..fca947cd 100644 --- a/maui/src/Charts/Series/ChartSeries.cs +++ b/maui/src/Charts/Series/ChartSeries.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Collections.Specialized; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; using Syncfusion.Maui.Toolkit.Graphics.Internals; namespace Syncfusion.Maui.Toolkit.Charts @@ -31,7 +27,7 @@ internal virtual ChartDataLabelSettings? ChartDataLabelSettings } } - internal readonly float DefaultSelectionStrokeWidth = 5;//Todo: check necessary this default value + internal readonly float _defaultSelectionStrokeWidth = 5;//Todo: check necessary this default value internal IChart? Chart { @@ -1622,7 +1618,7 @@ internal virtual void SetDashArray(ChartSegment segment) internal virtual Brush? GetFillColor(object item, int index) { - Brush? fillColor = null; + Brush? fillColor; // Chart selection check. fillColor = Chart?.GetSelectionBrush(this); @@ -1742,37 +1738,33 @@ internal bool CanAnimate() internal virtual void InvalidateSeries() { - var plotArea = Chart?.Area.PlotArea as ChartPlotArea; - - if (plotArea != null) - { - foreach (SeriesView seriesView in plotArea._seriesViews.Children) - { - if (seriesView != null && this == seriesView._series) - { - seriesView.InvalidateDrawable(); - break; - } - } - } - } + if (Chart?.Area.PlotArea is ChartPlotArea plotArea) + { + foreach (var view in plotArea._seriesViews.Children) + { + if (view is SeriesView seriesView && this == seriesView._series) + { + seriesView.InvalidateDrawable(); + break; + } + } + } + } internal void Invalidate() { - var plotArea = Chart?.Area.PlotArea as ChartPlotArea; - - if (plotArea != null) - { - foreach (SeriesView seriesView in plotArea._seriesViews.Children) - { - if (seriesView != null && this == seriesView._series) - { - seriesView.Invalidate(); - break; - } - } - } - } + if (Chart?.Area.PlotArea is ChartPlotArea plotArea) + { + foreach (var view in plotArea._seriesViews.Children) + { + if (view is SeriesView seriesView && this == seriesView._series) + { + seriesView.Invalidate(); + break; + } + } + } + } internal void ScheduleUpdateChart() { @@ -1865,12 +1857,12 @@ static void OnXBindingPathChanged(BindableObject bindable, object oldValue, obje { if (bindable is ChartSeries chartSeries) { - if (newValue != null && newValue is string) - { - chartSeries.XComplexPaths = ((string)newValue).Split(['.']); - } + if (newValue is string path) + { + chartSeries.XComplexPaths = path.Split(['.']); + } - chartSeries.OnBindingPathChanged(); + chartSeries.OnBindingPathChanged(); } } @@ -2091,17 +2083,15 @@ void Segments_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs void AddSegment(object chartSegment) { - var segment = chartSegment as ChartSegment; - - if (segment != null) - { - SetFillColor(segment); - SetStrokeColor(segment); - SetStrokeWidth(segment); - SetDashArray(segment); - segment.Opacity = (float)Opacity; - } - } + if (chartSegment is ChartSegment segment) + { + SetFillColor(segment); + SetStrokeColor(segment); + SetStrokeWidth(segment); + SetDashArray(segment); + segment.Opacity = (float)Opacity; + } + } void RemoveSegment(object chartSegment) { @@ -2129,11 +2119,9 @@ void OnAnimationPropertyChanged() void LabelStyle_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { - var labelStyle = sender as ChartDataLabelStyle; - - if (e.PropertyName != null && labelStyle != null && labelStyle.NeedDataLabelMeasure(e.PropertyName)) - { - InvalidateMeasureDataLabel(); + if (e.PropertyName != null && sender is ChartDataLabelStyle labelStyle && labelStyle.NeedDataLabelMeasure(e.PropertyName)) + { + InvalidateMeasureDataLabel(); } InvalidateDataLabel(); diff --git a/maui/src/Charts/Styles/ChartMarkerSettings.cs b/maui/src/Charts/Styles/ChartMarkerSettings.cs index 0aa3ac0e..5670c53d 100644 --- a/maui/src/Charts/Styles/ChartMarkerSettings.cs +++ b/maui/src/Charts/Styles/ChartMarkerSettings.cs @@ -31,7 +31,7 @@ /// /// /// - /// + /// /// /// /// diff --git a/maui/src/Charts/Utils/ChartUtils.cs b/maui/src/Charts/Utils/ChartUtils.cs index 4bce7168..381fb6c7 100644 --- a/maui/src/Charts/Utils/ChartUtils.cs +++ b/maui/src/Charts/Utils/ChartUtils.cs @@ -265,7 +265,7 @@ internal static bool SegmentContains(LineSegment segment, PointF touchPoint, Cha { var pointX = touchPoint.X; var pointY = touchPoint.Y; - var defaultSelectionStrokeWidth = series.DefaultSelectionStrokeWidth; + var defaultSelectionStrokeWidth = series._defaultSelectionStrokeWidth; var leftPoint = new PointF(pointX - defaultSelectionStrokeWidth, pointY - defaultSelectionStrokeWidth); var rightPoint = new PointF(pointX + defaultSelectionStrokeWidth, pointY + defaultSelectionStrokeWidth); var topPoint = new PointF(pointX + defaultSelectionStrokeWidth, pointY - defaultSelectionStrokeWidth); @@ -286,7 +286,7 @@ internal static bool SegmentContains(StepLineSegment segment, PointF touchPoint, { var pointX = touchPoint.X; var pointY = touchPoint.Y; - var defaultSelectionStrokeWidth = series.DefaultSelectionStrokeWidth; + var defaultSelectionStrokeWidth = series._defaultSelectionStrokeWidth; var leftPoint = new PointF(pointX - defaultSelectionStrokeWidth, pointY - defaultSelectionStrokeWidth); var rightPoint = new PointF(pointX + defaultSelectionStrokeWidth, pointY + defaultSelectionStrokeWidth); var topPoint = new PointF(pointX + defaultSelectionStrokeWidth, pointY - defaultSelectionStrokeWidth); @@ -310,7 +310,7 @@ internal static bool SegmentContains(SplineSegment segment, PointF touchPoint, C { var pointX = touchPoint.X; var pointY = touchPoint.Y; - var defaultSelectionStrokeWidth = series.DefaultSelectionStrokeWidth; + var defaultSelectionStrokeWidth = series._defaultSelectionStrokeWidth; var leftPoint = new PointF(pointX - defaultSelectionStrokeWidth, pointY - defaultSelectionStrokeWidth); var rightPoint = new PointF(pointX + defaultSelectionStrokeWidth, pointY + defaultSelectionStrokeWidth); var topPoint = new PointF(pointX + defaultSelectionStrokeWidth, pointY - defaultSelectionStrokeWidth); diff --git a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/EmptyPointSettingUnitTest.cs b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/EmptyPointSettingUnitTest.cs index ee5758a8..d0e680b3 100644 --- a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/EmptyPointSettingUnitTest.cs +++ b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/Features/EmptyPointSettingUnitTest.cs @@ -19,16 +19,18 @@ public void IsEmpty_SetValue_ReturnsExpectedValue(bool expectedIsEmpty) [Fact] public void ReValidateYValues_SetsValuesToNaN_Correctly() { - var columnSeries = new ColumnSeries(); - columnSeries.YValues = new List { 1, 2, 3, 4, 5 }; - IList[] emptyPointIndex = new IList[] - { - new List { 1, 3 }, - new List() - }; + var columnSeries = new ColumnSeries + { + YValues = [1, 2, 3, 4, 5] + }; + IList[] emptyPointIndex = + [ + [1, 3], + [] + ]; columnSeries.EmptyPointIndexes = emptyPointIndex; columnSeries.ResetEmptyPointIndexes(); - Assert.Equal(new List { 1.0, double.NaN, 3.0, double.NaN, 5.0 }, columnSeries.YValues); + Assert.Equal([1.0, double.NaN, 3.0, double.NaN, 5.0], columnSeries.YValues); } [Fact] @@ -45,17 +47,17 @@ public void EmptyPointIndexes_CanBeSetAndRetrieved() var series = new ColumnSeries(); var indexes = new List[] { - new List { 2, 4 }, - new List {3 }, - new List() + [2, 4], + [3], + [] }; series.EmptyPointIndexes = indexes; var retrievedIndexes = series.EmptyPointIndexes; Assert.NotNull(retrievedIndexes); Assert.Equal(3, retrievedIndexes.Length); - Assert.Equal(new List { 2, 4 }, retrievedIndexes[0]); - Assert.Equal(new List { 3 }, retrievedIndexes[1]); + Assert.Equal([2, 4], retrievedIndexes[0]); + Assert.Equal([3], retrievedIndexes[1]); Assert.Empty(retrievedIndexes[2]); } @@ -83,8 +85,10 @@ public void EmptyPointMode_DefaultValue_ShouldBeNone() [InlineData(EmptyPointMode.None)] public void EmptyPointMode_SetAndRetrieveValue(EmptyPointMode emptyPointMode) { - var series = new ColumnSeries(); - series.EmptyPointMode = emptyPointMode; + var series = new ColumnSeries + { + EmptyPointMode = emptyPointMode + }; var mode = series.EmptyPointMode; Assert.Equal(emptyPointMode, mode); } @@ -112,10 +116,13 @@ public void EmptyPointSettings_CanBeSetAndRetrieved() [Fact] public void ValidateYPoints_With10Points_EmptyPointMode_Average_ShouldReplaceNaNWithAverage() { - var series = new ColumnSeries() { YValues = new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 } }; - series.EmptyPointMode = EmptyPointMode.Average; + var series = new ColumnSeries + { + YValues = [1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0], + EmptyPointMode = EmptyPointMode.Average + }; series.ValidateYValues(); - Assert.Equal(new List { 1.0, 1.5, 2.0, 3.0, 4.25, 5.5, 6.0, 7.0, 8.5, 10.0 }, series.YValues); + Assert.Equal([1.0, 1.5, 2.0, 3.0, 4.25, 5.5, 6.0, 7.0, 8.5, 10.0], series.YValues); } [Fact] @@ -123,41 +130,47 @@ public void ValidateYPoints_With10Points_EmptyPointMode_Zero_ShouldReplaceNaNWit { var series = new ColumnSeries { - YValues = new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 }, + YValues = [1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0], EmptyPointMode = EmptyPointMode.Zero }; series.ValidateYValues(); - Assert.Equal(new List { 1.0, 0, 2.0, 3.0, 0, 5.5, 6.0, 7.0, 0, 10.0 }, series.YValues); + Assert.Equal([1.0, 0, 2.0, 3.0, 0, 5.5, 6.0, 7.0, 0, 10.0], series.YValues); } [Fact] public void ValidateDataPoints_With10Points_EmptyPointMode_None_ShouldRemainUnchanged() { - var series = new ColumnSeries(); - series.EmptyPointMode = EmptyPointMode.None; + var series = new ColumnSeries + { + EmptyPointMode = EmptyPointMode.None + }; var yValues = new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 }; series.ValidateDataPoints(yValues); - Assert.Equal(new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 }, yValues); + Assert.Equal([1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0], yValues); } [Fact] public void ValidateDataPoints_With10Points_EmptyPointMode_Zero_ShouldReplaceNaNWithZero() { - var series = new ColumnSeries(); - series.EmptyPointMode = EmptyPointMode.Zero; + var series = new ColumnSeries + { + EmptyPointMode = EmptyPointMode.Zero + }; var yValues = new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 }; series.ValidateDataPoints(yValues); - Assert.Equal(new List { 1.0, 0.0, 2.0, 3.0, 0.0, 5.5, 6.0, 7.0, 0.0, 10.0 }, yValues); + Assert.Equal([1.0, 0.0, 2.0, 3.0, 0.0, 5.5, 6.0, 7.0, 0.0, 10.0], yValues); } [Fact] public void ValidateDataPoints_With10Points_EmptyPointMode_Average_ShouldReplaceNaNWithAverage() { - var series = new ColumnSeries(); - series.EmptyPointMode = EmptyPointMode.Average; + var series = new ColumnSeries + { + EmptyPointMode = EmptyPointMode.Average + }; var yValues = new List { 1.0, double.NaN, 2.0, 3.0, double.NaN, 5.5, 6.0, 7.0, double.NaN, 10.0 }; series.ValidateDataPoints(yValues); - Assert.Equal(new List { 1.0, 1.5, 2.0, 3.0, 4.25, 5.5, 6.0, 7.0, 8.5, 10.0 }, yValues); + Assert.Equal([1.0, 1.5, 2.0, 3.0, 4.25, 5.5, 6.0, 7.0, 8.5, 10.0], yValues); } [Fact] From 7345cc4be7eaa83e8e81730d9e032a2ffd0a9f4f Mon Sep 17 00:00:00 2001 From: Harsha Date: Thu, 13 Mar 2025 11:06:01 +0530 Subject: [PATCH 5/5] cleared null exception --- .../SampleList/CartesianChartSamplesList.xml | 2 -- maui/src/Charts/Series/BoxAndWhiskerSeries.cs | 2 +- maui/src/Charts/Series/CartesianSeries.cs | 25 ++++++------------- maui/src/Charts/Series/ChartSeriesPartial.cs | 8 +++--- maui/src/Charts/Series/PieSeries.cs | 4 +-- maui/src/Charts/Series/RangeColumnSeries.cs | 4 +++ .../Charts/Series/SplineRangeAreaSeries.cs | 5 ++++ 7 files changed, 24 insertions(+), 26 deletions(-) diff --git a/maui/samples/Gallery/SampleList/CartesianChartSamplesList.xml b/maui/samples/Gallery/SampleList/CartesianChartSamplesList.xml index 864d6c9f..bf07f435 100644 --- a/maui/samples/Gallery/SampleList/CartesianChartSamplesList.xml +++ b/maui/samples/Gallery/SampleList/CartesianChartSamplesList.xml @@ -57,8 +57,6 @@ - - diff --git a/maui/src/Charts/Series/BoxAndWhiskerSeries.cs b/maui/src/Charts/Series/BoxAndWhiskerSeries.cs index f04838c8..f76f75da 100644 --- a/maui/src/Charts/Series/BoxAndWhiskerSeries.cs +++ b/maui/src/Charts/Series/BoxAndWhiskerSeries.cs @@ -743,7 +743,7 @@ internal override void GeneratePropertyPoints(string[] yPaths, IList[] y } } - internal override void SetIndividualPoint(int index, object obj, bool replace) + internal override void SetIndividualPoint(object obj, int index, bool replace) { if (YDataCollection != null && YPaths != null && ItemsSource != null) { diff --git a/maui/src/Charts/Series/CartesianSeries.cs b/maui/src/Charts/Series/CartesianSeries.cs index e94ffb75..63a2d359 100644 --- a/maui/src/Charts/Series/CartesianSeries.cs +++ b/maui/src/Charts/Series/CartesianSeries.cs @@ -1065,12 +1065,9 @@ internal override void OnDataSourceChanged(object oldValue, object newValue) InvalidateSideBySideSeries(); foreach (var item in EmptyPointIndexes) { - if (item != null) - - { - item.Clear(); - } + item?.Clear(); } + base.OnDataSourceChanged(oldValue, newValue); } @@ -1080,18 +1077,15 @@ internal override void OnDataSource_CollectionChanged(object? sender, NotifyColl base.OnDataSource_CollectionChanged(sender, e); } - internal override void AddDataPoint(int index, object data, NotifyCollectionChangedEventArgs e) + internal override void AddDataPoint(object data, int index, NotifyCollectionChangedEventArgs e) { ResetEmptyPointIndexes(); foreach (var item in EmptyPointIndexes) { - if (item != null) - - { - item.Clear(); - } + item?.Clear(); } - base.AddDataPoint(index, data, e); + + base.AddDataPoint(data, index, e); } internal override void RemoveData(int index, NotifyCollectionChangedEventArgs e) @@ -1099,12 +1093,9 @@ internal override void RemoveData(int index, NotifyCollectionChangedEventArgs e) ResetEmptyPointIndexes(); foreach (var item in EmptyPointIndexes) { - if (item != null) - - { - item.Clear(); - } + item?.Clear(); } + base.RemoveData(index, e); } diff --git a/maui/src/Charts/Series/ChartSeriesPartial.cs b/maui/src/Charts/Series/ChartSeriesPartial.cs index dd869f31..2e36f5d2 100644 --- a/maui/src/Charts/Series/ChartSeriesPartial.cs +++ b/maui/src/Charts/Series/ChartSeriesPartial.cs @@ -82,10 +82,10 @@ internal ChartValueType XValueType #region Internal Methods #pragma warning disable IDE0060 // Remove unused parameter - internal virtual void AddDataPoint(int index, object data, NotifyCollectionChangedEventArgs e) + internal virtual void AddDataPoint(object data, int index, NotifyCollectionChangedEventArgs e) #pragma warning restore IDE0060 // Remove unused parameter { - SetIndividualPoint(index, data, false); + SetIndividualPoint(data, index, false); } internal virtual void LegendItemToggled(LegendItem chartLegendItem) @@ -116,7 +116,7 @@ internal virtual void GenerateDataPoints() internal virtual void OnDataSource_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { IsDataPointAddedDynamically = false; - e.ApplyCollectionChanges((obj, index, canInsert) => AddDataPoint(index, obj, e), (obj, index) => RemoveData(index, e), ResetDataPoint); + e.ApplyCollectionChanges((obj, index, canInsert) => AddDataPoint(obj, index, e), (obj, index) => RemoveData(index, e), ResetDataPoint); if (e.Action == NotifyCollectionChangedAction.Add && EnableAnimation && AnimationDuration > 0) { @@ -219,7 +219,7 @@ internal void InvalidateGroupValues() //TODO:Need to remove the replace parameter from this method, //because new notify collectionChanged event first remove //the data and then insert the data. So no need replace parameter here after. - internal virtual void SetIndividualPoint(int index, object obj, bool replace) + internal virtual void SetIndividualPoint(object obj, int index, bool replace) { if (SeriesYValues != null && YPaths != null && ItemsSource != null) { diff --git a/maui/src/Charts/Series/PieSeries.cs b/maui/src/Charts/Series/PieSeries.cs index 27f5a834..6a6ec8b3 100644 --- a/maui/src/Charts/Series/PieSeries.cs +++ b/maui/src/Charts/Series/PieSeries.cs @@ -599,9 +599,9 @@ internal override void RemoveData(int index, NotifyCollectionChangedEventArgs e) CalculateGroupToYValues(); } - internal override void SetIndividualPoint(int index, object obj, bool replace) + internal override void SetIndividualPoint(object obj, int index, bool replace) { - base.SetIndividualPoint(index, obj, replace); + base.SetIndividualPoint(obj, index, replace); CalculateGroupToYValues(); } diff --git a/maui/src/Charts/Series/RangeColumnSeries.cs b/maui/src/Charts/Series/RangeColumnSeries.cs index e5058237..050f7bcb 100644 --- a/maui/src/Charts/Series/RangeColumnSeries.cs +++ b/maui/src/Charts/Series/RangeColumnSeries.cs @@ -417,6 +417,10 @@ internal override void GenerateTrackballPointInfo(List nearestDataPoints double yValue = yValues[index]; double yValue1 = yValues1[index]; string label = string.Format("{0} : {1:#.##}\n{2} : {3:#.##}", SfCartesianChartResources.High, yValue, SfCartesianChartResources.Low, yValue1); + if (yValue == 0 || yValue1 == 0) + { + label = string.Format("{0} : {1:0.##}\n{2} : {3:0.##}", SfCartesianChartResources.High, yValue, SfCartesianChartResources.Low, yValue1); + } if (IsSideBySide) { diff --git a/maui/src/Charts/Series/SplineRangeAreaSeries.cs b/maui/src/Charts/Series/SplineRangeAreaSeries.cs index 4557089e..527a6484 100644 --- a/maui/src/Charts/Series/SplineRangeAreaSeries.cs +++ b/maui/src/Charts/Series/SplineRangeAreaSeries.cs @@ -491,6 +491,11 @@ internal override void GenerateTrackballPointInfo(List nearestDataPoints double highValue = highValues[index]; double lowValue = lowValues[index]; string label = string.Format("{0} : {1:#.##}\n{2} : {3:#.##}", SfCartesianChartResources.High, highValue, SfCartesianChartResources.Low, lowValue); + if (highValue == 0 || lowValue == 0) + { + label = string.Format("{0} : {1:0.##}\n{2} : {3:0.##}", SfCartesianChartResources.High, highValue, SfCartesianChartResources.Low, lowValue); + } + var xPoint = TransformToVisibleX(xValue, topValue); var yPoint = TransformToVisibleY(xValue, topValue);