Search code examples
c#wpfmvvmlivecharts

WPF MVVM LiveCharts how to show X axis Label?


I'm having hard times to show some simple labels in my Cartesian Bar Charts, I'm reading a lot around but nothing seems to work for me. I'm using the MVVM pattern in my project, so this is the code I have so far..

VIEW

<lvc:CartesianChart Grid.Row="2" Series="{Binding ChartDataSets}">
    <lvc:CartesianChart.AxisX>
        <lvc:Axis LabelsRotation="20" Labels="{Binding ColumnLabels}" Position="RightTop" >
            <lvc:Axis.Separator >
                <lvc:Separator Step="1"></lvc:Separator>
            </lvc:Axis.Separator>
        </lvc:Axis>
    </lvc:CartesianChart.AxisX>
    <lvc:CartesianChart.AxisY>
        <lvc:Axis LabelFormatter="{Binding Formatter}" Position="RightTop"></lvc:Axis>
    </lvc:CartesianChart.AxisY>
</lvc:CartesianChart>

DataModel

class DataModel : INotifyPropertyChanged { private double value; public double Value { get => this.value; set { this.value = value; OnPropertyChanged(); } }

private string label;
public string Label
{
    get => this.label;
    set
    {
        this.label = value;
        OnPropertyChanged("Label");
    }
}

public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

}

ViewModel

class BackupStatsViewModel : INotifyPropertyChanged
{
    ChartValues<DataModel> values = new ChartValues<DataModel>();
    public SeriesCollection ChartDataSets { get; set; }
    public ObservableCollection<string> ColumnLabels { get; set; }
    public class ErrorPrt
    {
        public ErrorPrt(){
            prtName = string.Empty;
            Count = -1;
        }
        public string prtName { get; set; }
        public int Count { get; set; }
    }
    public BackupStatsViewModel()
    {
        InitializeBarChartData();
    }
    private void InitializeBarChartData()
    {

        this.ColumnLabels = new ObservableCollection<string>(values.Select(dataModel => dataModel.Label));
        var dataMapper = new CartesianMapper<DataModel>()
          .Y(dataModel => dataModel.Value)
          .Fill(dataModel => dataModel.Value > 15.0 ? Brushes.Red : Brushes.Green);

        this.ChartDataSets = new SeriesCollection
        {
          new ColumnSeries
          {
            Values = values,
            Configuration = dataMapper,
            DataLabels = true
          }
          
        };
    }
    public ErrorPrt[] PrtCount(List<DataRow> rows)
    {
        IEnumerable<IGrouping<string, DataRow>> grouped = rows.GroupBy(s => s.Field<string>(2));
        ErrorPrt[] err = new ErrorPrt[grouped.Count()];
        //Omitted code for sake of brevity
        
        ErrorPrt[] arr = err.Where(c => c != null).ToArray();
        
        for (int i = 0; i < arr.Count(); i++)
            values.Add(new DataModel() { Label = $"PRT {arr[i].prtName}", Value = arr[i].Count });

        return arr;
    }
 }

But as you can see no labels are shown on the X axis.. really don't know how to bypass this problem in order to go on with my work..please can anyone show me the right way? enter image description here


Solution

  • Your flow looks broken:
    you first initialize the chart data from the constructor by calling InitializeBarChartData(), which also initializes the ColumnLabels collection. Then you create the underlying ErrorPtr items, which are the provider of the data for the column labels.
    The result is that the ColumnLabels property is empty => no labels will be displayed.

    Because you add the new ErrorPtr items to the values field and this field is of type ChartValues and this collection implements INotifyCollectionChanged, the chart will reflect those changes. You were lucky here.

    But because you never update the ColumnLabels property after you have created the ErrorPtr items, the initially (after calling InitializeBarChartData from the constructor) empty ColumnLabels collection remains empty.

    Solution 1

    Fix the flow of your data model initialization and call InitializeBarChartData after PrtCount:

    public ErrorPrt[] PrtCount(List<DataRow> rows)
    {
        IEnumerable<IGrouping<string, DataRow>> grouped = rows.GroupBy(s => s.Field<string>(2));
        ErrorPrt[] err = new ErrorPrt[grouped.Count()];
        //Omitted code for sake of brevity
        
        ErrorPrt[] arr = err.Where(c => c != null).ToArray();
        
        for (int i = 0; i < arr.Count(); i++)
            this.values.Add(new DataModel() { Label = $"PRT {arr[i].prtName}", Value = arr[i].Count });
    
        // Initialize the chat models.
        // NOW the label data (the ErrorPrt.prtName) is generated 
        // and ready to be extracted from the ErrorPrt instances
        InitializeBarChartData();
    
        return arr;
    }
    

    Solution 2 (Recommended)

    Since all involved collections implement INotifyCollectionChanged you can dynamically update every collection when new data arrives. You don't need to initialize the complete chart data like the SeriesCollection and the Mapper or the label formatter over and over again (like in Solution 1 - in case PrtCount will be called more than once).
    You can continue to call InitializeBarChartData once from the constructor, like you are currently doing it.

    Simply don't only update the values field, but also the ColumnLabels property:

    public ErrorPrt[] PrtCount(List<DataRow> rows)
    {
        IEnumerable<IGrouping<string, DataRow>> grouped = rows.GroupBy(s => s.Field<string>(2));
        ErrorPrt[] err = new ErrorPrt[grouped.Count()];
        //Omitted code for sake of brevity
        
        ErrorPrt[] arr = err.Where(c => c != null).ToArray();        
    
        for (int i = 0; i < arr.Count(); i++)
        {    
            var newDataModel = new DataModel() { Label = $"PRT {arr[i].prtName}", Value = arr[i].Count };
    
            // Here you update the column values
            // and add the new items to the existing items of previous calls
            this.values.Add(newDataModel);
    
            // Also update the labels whenever new column data has arrived
            this.ColumnLabels.Add(newDataModel.Label);
        }
    
        return arr;
    }