Adding ChartSeries programmatically (MVVM)

Nov 17, 2014 at 8:48 AM
Edited Nov 18, 2014 at 10:34 AM
Hello

I've been searching a while now for a good looking chart visualization solution for my project and I finally found it so first of all a big thank you for creating and sharing this.

I've been playing around a little with the charts and I really love them. Very smooth and beautiful. There is 1 thing I need a little more information about and that is how I can add ChartSeries at runtime.

EDIT: I managed to add bars on runtime but there is some bug I can't seem to figure out. Let's start with an image:

Image

As you can see in the image, the titles on the axis are correct and scale when I scale the window. The problem here are the bars. The bars seem to have a fixed width and don't want to fit on the screen. The way I did this is as follow:

In XAML I placed the control like this:
<chart:StackedColumnChart ChartTitle="Total"
                              ChartSubTitle="(800)"
                              Series="{Binding Bars}">
</chart:StackedColumnChart>
In my code behind I added the Bars like this:
using System;
using System.Collections.Generic;
using De.TorstenMandelkow.MetroChart;
using System.Collections.ObjectModel;

namespace AutoShop
{
class OccupationChartGroupViewModel
{
    public OccupationChartGroupViewModel()
    {
        List<Tuple<string, int, double>> reqProdHoursPerWeek = DbServiceSegmentRequirement.GetProdSegReq();
        List<Tuple<string, int, double>> reqPlanHoursPerWeek = DbServiceSegmentRequirement.GetPlanSegReq();
        List<Tuple<string, int, double>> reqRepairHoursPerWeek = DbServiceSegmentRequirement.GetRepairSegReq();

        List<Tuple<string, int, double>> reqHoursPerWeek = new List<Tuple<string, int, double>>();
        reqHoursPerWeek.AddRange(reqProdHoursPerWeek);
        reqHoursPerWeek.AddRange(reqPlanHoursPerWeek);
        reqHoursPerWeek.AddRange(reqRepairHoursPerWeek);

        reqHoursPerWeek.Sort();

        switch (reqHoursPerWeek[0].Item2.ToString())
        {
            case "0":
                if (reqHoursPerWeek[1].Item2.ToString() != "1")
                    reqHoursPerWeek.Add(new Tuple<string, int, double>(reqHoursPerWeek[0].Item1, 1, 0.0));
                else
                    if (reqHoursPerWeek[1].Item1 != reqHoursPerWeek[0].Item1)
                        reqHoursPerWeek.Add(new Tuple<string, int, double>(reqHoursPerWeek[0].Item1, 1, 0.0));
                break;

            case "1":
                reqHoursPerWeek.Add(new Tuple<string, int, double>(reqHoursPerWeek[0].Item1, 0, 0.0));
                if (reqHoursPerWeek[1].Item2.ToString() != "2")
                    reqHoursPerWeek.Add(new Tuple<string, int, double>(reqHoursPerWeek[0].Item1, 2, 0.0));
                else
                    if (reqHoursPerWeek[1].Item1 != reqHoursPerWeek[0].Item1)
                        reqHoursPerWeek.Add(new Tuple<string, int, double>(reqHoursPerWeek[0].Item1, 2, 0.0));
                break;

            case "2":
                reqHoursPerWeek.Add(new Tuple<string, int, double>(reqHoursPerWeek[0].Item1, 0, 0.0));
                reqHoursPerWeek.Add(new Tuple<string, int, double>(reqHoursPerWeek[0].Item1, 1, 0.0));
                break;

            default: break;
        }

        reqHoursPerWeek.Sort();

        Bars = new ObservableCollection<De.TorstenMandelkow.MetroChart.ChartSeries>();

        ObservableCollection<TestClass> blocks = new ObservableCollection<TestClass>();

        foreach (Tuple<string, int, double> reqHours in reqHoursPerWeek)
        {
            string group = reqHours.Item2.ToString() == "2" ? "Order1" : reqHours.Item2.ToString() == "1" ? "Order2" : reqHours.Item2.ToString() == "0" ? "Order3" : "Unknown";

            blocks.Add(new TestClass() { Category = group, Number = reqHours.Item3 });

            if (reqHoursPerWeek.IndexOf(reqHours) + 1 < reqHoursPerWeek.Count)
            {
                if (reqHours.Item1 != reqHoursPerWeek[reqHoursPerWeek.IndexOf(reqHours) + 1].Item1)
                {
                    ChartSeries chartSerie = new ChartSeries();
                    chartSerie.SeriesTitle = reqHours.Item1;
                    chartSerie.DisplayMember = "Category";
                    chartSerie.ValueMember = "Number";
                    chartSerie.ItemsSource = blocks;
                    Bars.Add(chartSerie);

                    blocks = new ObservableCollection<TestClass>();
                }
            }
            else
            {
                ChartSeries chartSerie = new ChartSeries();
                chartSerie.SeriesTitle = reqHours.Item1;
                chartSerie.DisplayMember = "Category";
                chartSerie.ValueMember = "Number";
                chartSerie.ItemsSource = blocks;
                Bars.Add(chartSerie);
            }
        }
    }

    public ObservableCollection<ChartSeries> Bars { get; private set; }
}

// class which represent a data point in the chart
public class TestClass
{
    public string Category { get; set; }

    public double Number { get; set; }
}
}
Lastly, the error I get on design time in the XAML viewer is this one:
NullReferenceException: Object reference not set to an instance of an object.

Stacktrace
at De.TorstenMandelkow.MetroChart.ChartBase.UpdateDataContextOfSeries()
at De.TorstenMandelkow.MetroChart.ChartBase.InternalDataContextChanged()
at De.TorstenMandelkow.MetroChart.ChartBase.DataContextWatcher_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs args)
at System.Windows.DependencyObject.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
at System.Windows.FrameworkElement.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
at System.Windows.DependencyObject.NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
at System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType)
at System.Windows.DependencyObject.InvalidateProperty(DependencyProperty dp, Boolean preserveCurrentValue)
at System.Windows.Data.BindingExpressionBase.Invalidate(Boolean isASubPropertyChange)
at System.Windows.Data.BindingExpression.TransferValue(Object newValue, Boolean isASubPropertyChange)
at System.Windows.Data.BindingExpression.Activate(Object item)
at System.Windows.Data.BindingExpression.AttachToContext(AttachAttempt attempt)
at System.Windows.Data.BindingExpression.MS.Internal.Data.IDataBindEngineClient.AttachToContext(Boolean lastChance)
at MS.Internal.Data.DataBindEngine.Task.Run(Boolean lastChance)
at MS.Internal.Data.DataBindEngine.Run(Object arg)
at MS.Internal.Data.DataBindEngine.OnLayoutUpdated(Object sender, EventArgs e)
at System.Windows.ContextLayoutManager.fireLayoutUpdateEvent()
at System.Windows.ContextLayoutManager.UpdateLayout()
at System.Windows.UIElement.UpdateLayout()

InnerException: None
Jan 30, 2015 at 5:13 AM
Anyone?
Mar 12, 2015 at 5:48 PM
Edited Mar 12, 2015 at 5:48 PM
I had the same error, you can't have empty series binding


Try this in XAML:
<chart:StackedColumnChart Name="myChart" ChartTitle="Total"
                              ChartSubTitle="(800)">
  <chart:StackedColumnChart .Series>
                    <chart:ChartSeries/> 
                </chart:StackedColumnChart>
</chart:StackedColumnChart>
In codebehind CSS:
// Clear all current series (including the dummy 1st one the first time)
myChart.Series.Clear();
// Create and configure series
 ChartSeries series = new ChartSeries();
                series.SeriesTitle = "Title";
                series.DisplayMember = "someVar";
                series.ValueMember = "someVar";
// Important: if you want the graph to update when adding, removing or chaning series, set ItemsSource to null first (this will force it to update)
series.ItemsSource = null;

// Then add to chart and set to actual data source
myChart.Series.Add(series);
 ObservableCollection<ChartData> myData = new ObservableCollection<ChartData>();
series.ItemsSource = myData;
To restate: Important: if you want the graph to update when adding, removing or changing series dynamically, set ItemsSource to null first (this will force it to update)
Mar 13, 2015 at 11:21 PM
Thanks for the reply. It's working like a charm now.
May 22, 2015 at 8:57 AM
This worked for you Krowi? I'm having the same issue and I can't get it to work, because this example has nothing to do with MVVM anymore... I'm really desperate by now and would appreciate any help or hint how to get this to work.
May 22, 2015 at 4:29 PM
In my xaml (view) code I have the following:
<chart:StackedColumnChart Name="chart" ChartTitle="My Title" ChartSubTitle="{Binding ChartSubTitle}" SeriesSource="{Binding Path=Series}">
    <chart:StackedColumnChart.SeriesTemplate>
        <DataTemplate>
            <chart:ChartSeries SeriesTitle="{Binding Path=DisplayName}" DisplayMember="Category" ValueMember="Value" ItemsSource="{Binding Path=Items}" />
        </DataTemplate>
    </chart:StackedColumnChart.SeriesTemplate>
</chart:StackedColumnChart>
SeriesSource="{Binding Path=Series}" is the part with the data.

In my view model I have a property
public ObservableCollection<SeriesData> Series
    {
        get { return _series; }
        private set
        {
            _series = value;
            NotifyPropertyChanged();
        }
    }
The class SeriesData has the following properties (this is this part in xaml: SeriesTitle="{Binding Path=DisplayName}" and ItemsSource="{Binding Path=Items}"):
public string DisplayName { get; set; }
public ObservableCollection<GraphData> Items { get; set; }
The GraphData has these properties (this is this part in xaml: DisplayMember="Category" ValueMember="Value"):
public string Category { get; set; }
public double Value { get; set; }
So to add data to it you give a name to a category. This is the name of the columns in a bar chart. The value is obvious.

Little example:
var tempSeries = new ObservableCollection<SeriesData>();
var weekData = new ObservableCollection<GraphData>();
weekData.Add(new GraphData
{
    Category = "Column Name",
    Value = 10
});
tempSeries.Add(new SeriesData { DisplayName = week, Items = weekData });
Series = tempSeries;
The reason for the tempSeries is because some lag occurred when adding a lot of data to 1 graph. The reason for this I think is that the UI was constantly reloading cause the Series property was updated.

I hope the above helps. There should also be an example in the source somewhere but forgot where exactly.
May 26, 2015 at 11:39 AM
Hi Krowi,

thanks for your reply. I'm relatively new to the whole wpf universe. I understand what you are doing with the observable collections, the classes and also the DataBinding. Unfortunately I have no experience with Interfaces and don't know how to exactly implement the INotifyPropertyChanged one. Could you please show me, how you did it, so I can adopt it for my usage? I really would appreciate that.
May 26, 2015 at 11:22 PM
@kawsidw

Here's an implementation for INotifyPropertyChanged:
public class BaseModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        // Create the OnPropertyChanged method to raise the event 
        protected void OnPropertyChanged(string name)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(name));
            }
        }
    }
I suggest you look into MVVM frameworks such as MVVMLight or Caliburn Micro. If you're just beginning MVVM Light may be the way to go but both are good to learn from.
Jun 1, 2015 at 1:53 PM
Thanks everyone, it works now :)

Krowi, did you manage to solve you initial Problem with the bar sizes? I face the same problem now XD.
If I find a way, I'll post it here.
Nov 8, 2015 at 10:29 AM
Hi all,
I am having problem showing more than one series in any Column chart. I first to tried with hardcoded xaml series and then tried the approach above for an automated series addition. Both ways ended up with the same result. I try to make a small example with two series but I get the following result:

Image

Only one of my two series is shown, although space is allocated for the second one.
My console output shows the following:
System.Windows.Data Error: 40 : BindingExpression path error: 'ChartSubTitle' property not found on 'object' ''TotalChart' (Name='')'. BindingExpression:Path=ChartSubTitle; DataItem='TotalChart' (Name=''); target element is 'ClusteredColumnChart' (Name='chart'); target property is 'ChartSubTitle' (type 'String')
System.Windows.Data Error: 40 : BindingExpression path error: 'ChartLegendItemStyle' property not found on 'object' ''ContentPresenter' (Name='')'. BindingExpression:Path=ChartLegendItemStyle; DataItem='ContentPresenter' (Name=''); target element is 'ChartLegendItem' (Name=''); target property is 'Style' (type 'Style')
System.Windows.Data Error: 40 : BindingExpression path error: 'ChartLegendItemStyle' property not found on 'object' ''ContentPresenter' (Name='')'. BindingExpression:Path=ChartLegendItemStyle; DataItem='ContentPresenter' (Name=''); target element is 'ChartLegendItem' (Name=''); target property is 'Style' (type 'Style')
The code I use is the following:
<Window x:Class="FinanceHelper.TotalChart"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FinanceHelper"
        xmlns:chart="clr-namespace:De.TorstenMandelkow.MetroChart;assembly=De.TorstenMandelkow.MetroChart"
        mc:Ignorable="d"
        Title="TotalChart" Height="500" Width="900">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <chart:ClusteredColumnChart Name="chart" ChartTitle="My Title" ChartSubTitle="{Binding ChartSubTitle}" SeriesSource="{Binding Path=Series}">
            <chart:ClusteredColumnChart.SeriesTemplate>
                <DataTemplate>
                    <chart:ChartSeries DisplayMember="month" ValueMember="balance" ItemsSource="{Binding Path=MonthBalanceCollection}" />
                </DataTemplate>
            </chart:ClusteredColumnChart.SeriesTemplate>
        </chart:ClusteredColumnChart>
    </Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace FinanceHelper
{
    /// <summary>
    /// Interaction logic for TotalChart.xaml
    /// </summary>
    public partial class TotalChart : Window
    {
        public ObservableCollection<AccountSeriesData> Series { get; set; }

        public TotalChart(FinanceModel model)
        {
            InitializeComponent();
            this.DataContext = this;

            this.Series = this.calculateChartSeries();
                        
        }

        private ObservableCollection<AccountSeriesData> calculateChartSeries()
        {
            var tempSeries = new ObservableCollection<AccountSeriesData>();

            var savings = new AccountSeriesData();
            savings.AccountName = "Savings";
            savings.MonthBalanceCollection.Add(new MonthChartDetails() { month = "Jun", balance = 4000 });
            savings.MonthBalanceCollection.Add(new MonthChartDetails() { month = "Jul", balance = 5000 });
            savings.MonthBalanceCollection.Add(new MonthChartDetails() { month = "Aug", balance = 5500 });
            savings.MonthBalanceCollection.Add(new MonthChartDetails() { month = "Sep", balance = 6000 });

            var normal = new AccountSeriesData();
            normal.AccountName = "Normal";
            normal.MonthBalanceCollection.Add(new MonthChartDetails() { month = "Jun", balance = 1000 });
            normal.MonthBalanceCollection.Add(new MonthChartDetails() { month = "Jul", balance = 2000 });
            normal.MonthBalanceCollection.Add(new MonthChartDetails() { month = "Aug", balance = 3500 });
            normal.MonthBalanceCollection.Add(new MonthChartDetails() { month = "Sep", balance = 4000 });

            tempSeries.Add(savings);
            tempSeries.Add(normal);

            return tempSeries;
        }
    }

    public class AccountSeriesData
    {
        public string AccountName { get; set; }
        public ObservableCollection<MonthChartDetails> MonthBalanceCollection { get; set; }
        public AccountSeriesData()
        {
            MonthBalanceCollection = new ObservableCollection<MonthChartDetails>();
        }
    }

    public class MonthChartDetails
    {
        public string month { get; set; }
        public float balance { get; set; }
    }
}
I have tried many different options but I always end up at the same spot. I tried with Pie chart and that displays different pies for each series. But ofc pie chart is not what I want for my data.
I am also wondering because it seems that some properties of the charts (e.g. SeriesTitle) are not recognized and I have to remove them from my xaml. It feels like the added package for the charts is not completely recognized. I use .NET Framework 4.5.2 MetroChart version 1.0.0.0.

Any help is welcome!