SfCartesianChart with dynamic number of Series

103 views Asked by At

I am trying to display a dynamic number of series in dotnet maui with SfCartesianChart (Version 22.2.12).

All the data from the chart are present in the ViewModel like:

public class ChartPageViewModel : BaseViewModel, IChartPageViewModel
{
    //Data are loaded asynchronously after the page load
    //after setting the data the PropertyChangedEvent is fired
    public ObservableCollection<IChartSeriesViewModel> DataSeries { get; set; }
}

public class ChartSeriesViewModel : IChartSeriesViewModel
{
    public string Title { get; set; }
    public IList<IChartPointViewModel> Data { get; set; }
}
    
public class ChartPointViewModel : IChartPointViewModel
{
    public DateTime Date { get; set; }
    public decimal Value { get; set; }
}

The data is bound with the series-Property using a Value-Converter:

<chart:SfCartesianChart Series="{Binding DataSeries, Converter={StaticResource DataSeriesConverter}}">
     <chart:SfCartesianChart.XAxes>
         <chart:CategoryAxis />
     </chart:SfCartesianChart.XAxes>
     <chart:SfCartesianChart.YAxes>
         <chart:NumericalAxis />
     </chart:SfCartesianChart.YAxes>
 </chart:SfCartesianChart>

the Value-Converter (IList to ChartSeriesCollection)

public class SfCartesianChartDataSeriesConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is IList<IChartSeriesViewModel> chartSeries)
        {
            ChartSeriesCollection series = new ChartSeriesCollection();

            foreach (var i in chartSeries)
            {
                series.Add(new FastLineSeries()
                {
                    ItemsSource = i.Data,
                    XBindingPath = nameof(IChartPointViewModel.Date),
                    YBindingPath = nameof(IChartPointViewModel.Value),
                });
            }

            return series;
        }

        return null;
    }

Unfortunately, the data is only shown/drawn when the view are redrawn (e.g. with a window resize).

The PropertyChangedEvent seems to work fine, because when I add a series in xaml the data is displayed immediately after the PropertyChangedEvent.

<chart:SfCartesianChart>
    <chart:SfCartesianChart.XAxes>
        <chart:CategoryAxis />
    </chart:SfCartesianChart.XAxes>
    <chart:SfCartesianChart.YAxes>
        <chart:NumericalAxis />
    </chart:SfCartesianChart.YAxes>

    <chart:FastLineSeries
           ItemsSource="{Binding DataSeries[0].Data}"
           XBindingPath="Date"
           YBindingPath="Value" />
</chart:SfCartesianChart>

But this is not the way to create a dynamic number of series.

How can the "redraw" be forced? Or how must the binding be made for this case?

2

There are 2 answers

0
Christoph On BEST ANSWER

Based on the answer from user22357370, I have found the following solution. Modified to keep the solution generic.

Custom control, bases on SfCartesianChart:

public class SfCartesianChartExt : SfCartesianChart
{
    public static readonly BindableProperty SeriesCollectionProperty = BindableProperty.Create(
        nameof(SeriesCollection),
        typeof(ChartSeriesCollection),
        typeof(SfCartesianChartExt), null, BindingMode.Default, null, OnSeriesPropertyChanged);

    public ChartSeriesCollection SeriesCollection
    {
        get => (ChartSeriesCollection)GetValue(SeriesCollectionProperty);
        set => SetValue(SeriesCollectionProperty, value);
    }

    private static void OnSeriesPropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        (bindable as SfCartesianChartExt)?.GenerateSeries(newValue);
    }

    private void GenerateSeries(object newValue)
    {
        if (newValue is ChartSeriesCollection collection)
        {
            ((INotifyCollectionChanged)newValue).CollectionChanged += DataPoint_CollectionChanged;

            Series.Clear();

            foreach (var item in collection)

            {
                CreateSeries(item);
            }
        }
    }

    private void CreateSeries(ChartSeries item)
    {
        Series.Add(item);
    }

    private void DataPoint_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
            {
                if (e.NewItems != null)
                {
                    CreateSeries(e.NewItems[0] as ChartSeries);
                }
                break;
            }
            case NotifyCollectionChangedAction.Remove:
            {
                Series.RemoveAt(e.OldStartingIndex);
                break;
            }
        }
    }
}

Usage, with converter from question:

<chart:SfCartesianChartExt Series="{Binding DataSeries, Converter={StaticResource DataSeriesConverter}}">
     <chart:SfCartesianChart.XAxes>
         <chart:CategoryAxis />
     </chart:SfCartesianChart.XAxes>
     <chart:SfCartesianChart.YAxes>
         <chart:NumericalAxis />
     </chart:SfCartesianChart.YAxes>
 </chart:SfCartesianChartExt>
1
user22357370 On

A sample has been prepared based on your input. This requirement has been achieved by extending the CartesianChart. The sample is attached below for reference.

public class CartesianExt :SfCartesianChart
    {
        public static readonly BindableProperty SeriesCollectionProperty = BindableProperty.Create("SeriesCollection", typeof(ObservableCollection<IChartSeriesViewModel>), typeof(CartesianExt), null, BindingMode.Default, null, OnSeriesPropertyChanged);


        public ObservableCollection<IChartSeriesViewModel> SeriesCollection
        {
            get
            {
                return (ObservableCollection<IChartSeriesViewModel>)GetValue(SeriesCollectionProperty);
            }
            set { SetValue(SeriesCollectionProperty, value);}
        }

        private static void OnSeriesPropertyChanged(BindableObject bindable, object oldValue, object newValue)
        {
            (bindable as CartesianExt).GenerateSeries(newValue);
        }

        private void GenerateSeries(object newValue)
        {
           if(newValue is ObservableCollection<IChartSeriesViewModel> collection)
           {              
                if (newValue is INotifyCollectionChanged)
                    (newValue as INotifyCollectionChanged).CollectionChanged += DataPoint_CollectionChanged;
               foreach(var item in collection)
                {
                    CreateSeries(item);
                }
            }
        }

        private void CreateSeries(IChartSeriesViewModel item)
        {
            FastLineSeries series = new FastLineSeries()
            {
                ItemsSource = item.LiveData,
                XBindingPath = nameof(IChartPointViewModel.Date),
                YBindingPath = nameof(IChartPointViewModel.Value),
            };

            Series.Add(series);
        }

        private void DataPoint_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    CreateSeries(e.NewItems[0] as IChartSeriesViewModel);
                    break;
                case NotifyCollectionChangedAction.Remove:
                    Series.RemoveAt(e.OldStartingIndex);
                    break;
            }
         }
      }

public ChartSeriesViewModel()
    {
        LiveData = new ObservableCollection<IChartPointViewModel>();
      
  for (int i = 0; i < 100; i++)
        {
            LiveData.Add(new ChartPointViewModel() { Date = new DateTime(2005, 04, 02).AddMonths(i), Value = new Random().Next(0, 1000) });
        }
    }

public class ChartPageViewModel:ChartSeriesViewModel
{   
    public ObservableCollection<IChartSeriesViewModel> DataSeries { get; set; }
  
    public ChartPageViewModel()
    {
        DataSeries=new ObservableCollection<IChartSeriesViewModel>();

        DataSeries.Add(new ChartSeriesViewModel());
    }
}

<VerticalStackLayout >
        <local:CartesianExt SeriesCollection="{Binding DataSeries}" x:Name="Chart" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
        <chart:SfCartesianChart.XAxes>
            <chart:CategoryAxis/>
        </chart:SfCartesianChart.XAxes>
        <chart:SfCartesianChart.YAxes>
            <chart:NumericalAxis/>
        </chart:SfCartesianChart.YAxes>
    </local:CartesianExt>
        <Button Text="DynamicList" Clicked="Button_Clicked"></Button>
    </VerticalStackLayout>
private void Button_Clicked(object sender, EventArgs e)
    {
        chartPageViewModel.DataSeries.Add(new ChartSeriesViewModel());
    }

Output