MrShoes
MrShoes

Reputation: 485

"No suitable axis is available for plotting the dependent value" using MVVM with multiple dictionaries

I am trying to create a scatter series using the charting toolkit where the point's colour is defined by the count of entries at those coordinates. Basically, I have a GraphPointViewModel class with a Count(int) property, and a brush Colour property, which works fine.

Within the ViewModel that defines the scatter series, I have the following property:

public ObservableDictionary<int, ObservableDictionary<int, GraphPointViewModel>> Data { get; set; }

This ObservableDictionary simply adds the INotifyPropertyChanged methods to a Dictionary class, you notice that the Dictionary has another Dictionary. The reason is that the base Dictionary's Key is the X axis value, while the child Dictionary's key is the Y axis value. The GraphPointViewModel is then used to define the style of the series (i.e. colour the point as per requirements).

Populating of this series is proven to work fine. The problem comes from binding to the Y axis.

My chart is from the WPF Charting Toolkit (System.Windows.Controls.DataVisualization) and is defined in XAML as:

        <chartingToolkit:Chart Grid.RowSpan="2" Name="GrapherChart">

        <chartingToolkit:Chart.Axes>
            <chartingToolkit:LinearAxis Orientation="X" Interval="50" Minimum="{Binding Xmin}" Maximum="{Binding Xmax}" FontSize="10" Title="Phase"/>
            <chartingToolkit:LinearAxis Orientation="Y" Interval="500" Minimum="{Binding Ymin}" Maximum="{Binding Ymax}" FontSize="10" ShowGridLines="True" Title="Amplitude"/>
        </chartingToolkit:Chart.Axes>

        <chartingToolkit:ScatterSeries
            DependentValuePath="Value.Key"
            IndependentValuePath="Key"
            ItemsSource="{Binding Data}">
            <chartingToolkit:ScatterSeries.DataPointStyle>
                <Style TargetType="chartingToolkit:ScatterDataPoint">
                    <Setter Property="Background" Value="{Binding Value.Value.Colour}"></Setter>
                </Style>
            </chartingToolkit:ScatterSeries.DataPointStyle>
        </chartingToolkit:ScatterSeries>

        <chartingToolkit:Chart.LegendStyle>
            <Style TargetType="Control">
                <Setter Property="Width" Value="0"/>
                <Setter Property="Height" Value="0"/>
            </Style>
        </chartingToolkit:Chart.LegendStyle>
    </chartingToolkit:Chart>

When I run my application I get the error "No suitable axis is available for plotting the dependent value" even when no data has been added. I have tried changing the DependentValuePath to Key instead, but this simply uses the same Key as is used for the X axis. How can I bind dictionaries like this to achieve the effect I need?

UPDATE Having been asked for posting ViewModel classes (although I don't think they'll help), here's the GraphPointViewModel:

public class GraphPointViewModel : Mvvm.ViewModel
{

    private int _count;

    public int Count
    {
        get { return _count; }
        set { SetPropertyAndNotify(ref _count, value, new []{"Colour"}); }
    }

    public Brush Colour
    {
        get
        {
            return Count >= 5 ?
                Brushes.Black :
                Brushes.Gray;
        }
    }
}

Here's the ViewModel responsible for collating the graph data:

    public class EventGrapherViewModel : DataHandlingViewModel
{
    private double _xmin;

    public double Xmin
    {
        get { return _xmin; }
        set { SetPropertyAndNotify(ref _xmin, value); }
    }

    private double _ymin = -2500;

    public double Ymin
    {
        get { return _ymin; }
        set { SetPropertyAndNotify(ref _ymin, value); }
    }

    private double _zmin;

    public double Zmin
    {
        get { return _zmin; }
        set { SetPropertyAndNotify(ref _zmin, value); }
    }

    private double _xmax = 400;

    public double Xmax
    {
        get { return _xmax; }
        set { SetPropertyAndNotify(ref _xmax, value); }
    }

    private double _ymax = 2500;

    public double Ymax
    {
        get { return _ymax; }
        set { SetPropertyAndNotify(ref _ymax, value); }
    }

    private double _zmax;

    public double Zmax
    {
        get { return _zmax; }
        set { SetPropertyAndNotify(ref _zmax, value); }
    }

    public ObservableDictionary<int, ObservableDictionary<int, GraphPointViewModel>> Data { get; set; }

    /// <summary>
    /// The current channel being graphed. 
    /// Note that less than or equal to 0 means no event data is being graphed.
    /// </summary>
    private int _currentChannel;

    public EventGrapherViewModel()
    {
        var syncLock = new object();

        Data = new ObservableDictionary<int, ObservableDictionary<int, GraphPointViewModel>>();

        // EnableCollectionSynchronization allows updating of the UI when the collection is changed from a background thread.
        BindingOperations.EnableCollectionSynchronization(Data, syncLock);
    }

    public void AddDataPoint(int angle, int amplitude)
    {
        try
        {
            if (!Data.ContainsKey(angle))
            {
                Data.Add(angle, new ObservableDictionary<int, GraphPointViewModel>());
            }

            var matchingAngle = Data[angle];

            if (!matchingAngle.ContainsKey(amplitude))
            {
                matchingAngle.Add(amplitude, new GraphPointViewModel());
            }

            var matchingAmplitudeViewModel = matchingAngle.First(ma => ma.Key == amplitude).Value;
            matchingAmplitudeViewModel.Count++;
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }

    protected override void GrapherSelectionChanged(GrapherSelection grapherSelection)
    {
        Data.Clear();
        _currentChannel = (int)grapherSelection;
    }

    public override void AddDataPoint(Data dataPoint)
    {
        if (_currentChannel <= 0)
            return;

        var eventData = dataPoint as EventData;
        if (eventData == null)
            return;

        foreach (var eventDataPoint in eventData.EventDataPoints)
        {
            AddDataPoint(eventDataPoint.PhaseAngle, eventDataPoint.AmplitudesByChannel[_currentChannel]);
        }
    }

Data is received via MQTT and is then posted using the Broker, so the ViewModel has subscribed directly to that.

Upvotes: 0

Views: 1313

Answers (1)

MrShoes
MrShoes

Reputation: 485

I have fixed this by removing the two dictionaries. I now have an EventDataPointsViewModel class:

public class EventDataPointsViewModel : Mvvm.ViewModel
{

    private int _phaseAngle;

    public int PhaseAngle
    {
        get { return _phaseAngle; }
        set { SetPropertyAndNotify(ref _phaseAngle, value); }
    }

    private int _amplitude;

    public int Amplitude
    {
        get { return _amplitude; }
        set { SetPropertyAndNotify(ref _amplitude, value); }
    }

    private int _count;

    public int Count
    {
        get { return _count; }
        set { SetPropertyAndNotify(ref _count, value, new[] { "Colour" }); }
    }

    public Brush Colour
    {
        get
        {
            return Count >= 5 ?
                Brushes.Black :
                Brushes.Gray;
        }
    }
}

The data is now

public ObservableCollection<EventDataPointsViewModel> Data { get; set; }

And the AddDataPoint method is

    public void AddDataPoint(int angle, int amplitude)
    {
        try
        {
            var dataPoint = Data.FirstOrDefault(d => d.PhaseAngle == angle && d.Amplitude == amplitude);
            if (dataPoint != null)
            {
                dataPoint.Count++;
            }
            else
            {
                Data.Add(new EventDataPointsViewModel { Amplitude = amplitude, PhaseAngle = angle, Count = 1 });
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }

The chart's XAML is

    <chartingToolkit:Chart Grid.RowSpan="2" Name="GrapherChart">

        <chartingToolkit:Chart.Axes>
            <chartingToolkit:LinearAxis Orientation="X" Interval="50" Minimum="{Binding Xmin}" Maximum="{Binding Xmax}" FontSize="10" Title="Phase"/>
            <chartingToolkit:LinearAxis Orientation="Y" Interval="500" Minimum="{Binding Ymin}" Maximum="{Binding Ymax}" FontSize="10" ShowGridLines="True" Title="Amplitude"/>
        </chartingToolkit:Chart.Axes>

        <chartingToolkit:ScatterSeries
            DependentValuePath="Amplitude"
            IndependentValuePath="PhaseAngle"
            ItemsSource="{Binding Data}">
            <chartingToolkit:ScatterSeries.DataPointStyle>
                <Style TargetType="chartingToolkit:ScatterDataPoint">
                    <Setter Property="Background" Value="{Binding Colour}"></Setter>
                </Style>
            </chartingToolkit:ScatterSeries.DataPointStyle>
        </chartingToolkit:ScatterSeries>

        <chartingToolkit:Chart.LegendStyle>
            <Style TargetType="Control">
                <Setter Property="Width" Value="0"/>
                <Setter Property="Height" Value="0"/>
            </Style>
        </chartingToolkit:Chart.LegendStyle>
    </chartingToolkit:Chart>

Upvotes: 0

Related Questions