SciChart WPF 2D Charts > Tutorials > MVVM > Tutorial 09b - Linking Multiple Charts with MVVM
Tutorial 09b - Linking Multiple Charts with MVVM

In our series of tutorials, up until now we have added a chart with two YAxis, one XAxis, two series, added tooltips, legends and zooming, panning behavior, and added some annotations.

Source code for this tutorial can be found at our SciChart.WPF.Examples Github Repository under Tutorials section.

Next, we are going to show you how to create multiple charts and link them together.

Revision

If you haven't already you will need to review the following tutorials, as we're working straight from these:

Or, if you're in a hurry, skim through the tutorials and copy paste the code into a new solution. Once you've done that, we are ready to get started.

 

Refactoring our Code to go Multi Chart

At the moment our code consists of a MainWindow.xaml with a single SciChartSurface declared, and a MainViewModel with properties exposed for that chart surface. We are going to refactor this code a little bit to allow us to go multi chart.

First, create a new class called ChartViewModel and move the following code to it.

ChartViewModel
Copy Code
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Media;
using SciChart.Charting.Model.ChartSeries;
using SciChart.Charting.Model.DataSeries;
using SciChart.Charting.Visuals.Axes;
using SciChart.Data.Model;
namespace SciChart.Mvvm.Tutorial
{
    public class ChartViewModel : BindableObject
    {
        private readonly IDataProvider _dataProvider;
        private string _chartTitle;
        private readonly ObservableCollection<IRenderableSeriesViewModel> _renderableSeries = new ObservableCollection<IRenderableSeriesViewModel>();
        private readonly ObservableCollection<IAxisViewModel> _yAxes = new ObservableCollection<IAxisViewModel>();
        private readonly ObservableCollection<IAxisViewModel> _xAxes = new ObservableCollection<IAxisViewModel>();
        private readonly ObservableCollection<IAnnotationViewModel> _annotations = new ObservableCollection<IAnnotationViewModel>();
        private XyDataSeries<double, double> _lineData;       
        public ChartViewModel(IDataProvider dataProvider, string chartTitle, string mouseEventGroupId)
        {
            _dataProvider = dataProvider;
            _chartTitle = chartTitle;
            MouseEventGroup = mouseEventGroupId;
            CreateChartData();
            CreateChartSeries();
            CreateChartAxis();
            // Subscribe to future updates
            int i = 0;
            _dataProvider.SubscribeUpdates((newValues) =>
            {
                // Append when new values arrive
                _lineData.Append(newValues.XValues, newValues.YValues);
                // Zoom the chart to fit
                _lineData.InvalidateParentSurface(RangeMode.ZoomToFit);
                // Every 100th datapoint, add an annotation
                if (i % 100 == 0)
                {
                    Annotations.Add(new InfoAnnotationViewModel() { X1 = _lineData.XValues.Last(), Y1 = 0.0 });
                }
                i++;
            });
        }
        public string MouseEventGroup { get; set; }
        public ObservableCollection<IAxisViewModel> YAxes { get { return _yAxes; } }
        public ObservableCollection<IAxisViewModel> XAxes { get { return _xAxes; } }
        public ObservableCollection<IAnnotationViewModel> Annotations { get { return _annotations; } }
        public ObservableCollection<IRenderableSeriesViewModel> RenderableSeries { get { return _renderableSeries; } }
        private void CreateChartAxis()
        {
            YAxes.Add(new NumericAxisViewModel()
            {
                AutoRange = AutoRange.Always,
                AxisTitle = "Left YAxis",
                Id = "LeftYAxis",
                AxisAlignment = AxisAlignment.Left,
            });
            YAxes.Add(new NumericAxisViewModel()
            {
                AutoRange = AutoRange.Always,
                AxisTitle = "Right YAxis",
                AxisAlignment = AxisAlignment.Right,
            });
            XAxes.Add(new NumericAxisViewModel()
            {
                AutoRange = AutoRange.Always,
                AxisTitle = "XAxis",
                AxisAlignment = AxisAlignment.Bottom,
            });
        }
        private void CreateChartData()
        {
            var initialDataValues = _dataProvider.GetHistoricalData();
            // Create a DataSeries. We later apply this to a RenderableSeries
            _lineData = new XyDataSeries<double, double>() { SeriesName = _chartTitle + " Line Series" };
            // Append some data to the chart                                
            _lineData.Append(initialDataValues.XValues, initialDataValues.YValues);
        }
        private void CreateChartSeries()
        {
            // Create a RenderableSeries. Apply the DataSeries created before
            RenderableSeries.Add(new LineRenderableSeriesViewModel()
            {
                StrokeThickness = 2,
                Stroke = Colors.SteelBlue,
                DataSeries = _lineData,
                StyleKey = "LineSeriesStyle"
            });
        }               
        public string ChartTitle
        {
            get { return _chartTitle; }
            set
            {
                _chartTitle = value;
                OnPropertyChanged("ChartTitle");
            }
        }
    }
}

Now we need to adjust our MainViewModel to expose a collection of charts.

MainViewModel
Copy Code
using System.Collections.ObjectModel;
using SciChart.Data.Model;
namespace SciChart.Mvvm.Tutorial
{
    public class MainViewModel : BindableObject
    {        
        private bool _enablePan;
        private bool _enableZoom = true;
        private readonly ObservableCollection<ChartViewModel> _chartViewModels = new ObservableCollection<ChartViewModel>();
        public MainViewModel()
        {
            ChartPanes.Add(new ChartViewModel(new DummyDataProvider(), "Primary Chart", "MouseGroup1"));
            ChartPanes.Add(new ChartViewModel(new DummyDataProvider(), "Secondary Chart", "MouseGroup1"));
        }  
       
        public ObservableCollection<ChartViewModel> ChartPanes {  get { return _chartViewModels; } }
       
        public bool EnableZoom
        {
            get { return _enableZoom; }
            set
            {
                if (_enableZoom != value)
                {
                    _enableZoom = value;
                    OnPropertyChanged("EnableZoom");
                    if (_enableZoom) EnablePan = false;
                }
            }
        }
       
        public bool EnablePan
        {
            get { return _enablePan; }
            set
            {
                if (_enablePan != value)
                {
                    _enablePan = value;
                    OnPropertyChanged("EnablePan");
                    if (_enablePan) EnableZoom = false;
                }
            }
        }
    }
}

That's fantastic. We've refactored our code to enable multiple chart panes. The last thing we need to do is to update our MainWindow.xaml to bind to the new ChartPanes property:

MainView.xaml
Copy Code
<Window x:Class="SciChart.Mvvm.Tutorial.MainWindow"
        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:SciChart.Mvvm.Tutorial"
        xmlns:s="http://schemas.abtsoftware.co.uk/scichart"
        mc:Ignorable="d"
        Title="MainWindow" Height="550" Width="800">
    <Window.Resources>
       
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/SciChart.Charting;component/Themes/Default.xaml"/>
            </ResourceDictionary.MergedDictionaries>
            <local:MainViewModel x:Key="MainViewModel"/>
            <!-- The TooltipControl template is defined below -->
            <!-- Change this if you want to have a non-default tooltip container -->
            <!-- The ContentPresenter is bound to the DataContext (a SeriesInfo type) -->
            <!-- and the ContentTemplate is the DataTemplate for the SeriesInfo -->
            <!-- Finally, the TooltipContainerStyle is set on the RenderableSeries via Style -->
            <Style x:Key="TooltipContainer" TargetType="s:TooltipControl">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="s:TooltipControl">
                            <Border Background="#ff6495ed"
                                BorderBrush="#ff87B1FA"
                                BorderThickness="2"
                                CornerRadius="5"
                                Opacity="0.9"
                                Padding="5">
                                <ContentPresenter Content="{TemplateBinding DataContext}"
                                              ContentTemplate="{TemplateBinding ContentTemplate}" />
                            </Border>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>

            <!-- Tooltip Template for an XyDataSeries binds to XySeriesInfo -->
            <!-- Check out the properties on XySeriesInfo to see what you can bind to -->
            <DataTemplate x:Key="TooltipTemplate" DataType="s:XySeriesInfo">
                <StackPanel Orientation="Vertical">
                    <TextBlock Foreground="White">
                    <Run Text="Series: "/>
                    <Run Text="{Binding SeriesName, StringFormat='{}{0}:'}"/>
                    </TextBlock>
                    <TextBlock Foreground="White">
                    <Run Text="X-Value: "/>
                    <Run Text="{Binding FormattedXValue, Mode=OneWay}"/>
                    </TextBlock>
                    <TextBlock Foreground="White">
                    <Run Text="Y-Value: "/>
                    <Run Text="{Binding FormattedYValue, Mode=OneWay}"/>
                    </TextBlock>
                </StackPanel>
            </DataTemplate>
            <!-- Style applied via x:Key name in ViewModel. See StyleKey property of LineRenderableSeriesViewModel -->
            <Style TargetType="s:BaseRenderableSeries"  x:Key="LineSeriesStyle">
                <Setter Property="s:RolloverModifier.TooltipContainerStyle" Value="{StaticResource TooltipContainer}"/>
                <Setter Property="s:RolloverModifier.TooltipTemplate" Value="{StaticResource TooltipTemplate}"/>
                <Setter Property="s:RolloverModifier.IncludeSeries" Value="True"/>
                <Setter Property="StrokeThickness" Value="2"/>
            </Style>
            <Style TargetType="local:InfoAnnotation" BasedOn="{StaticResource MvvmBaseAnnotationStyle}"/>
        </ResourceDictionary>       
    </Window.Resources>
    <Grid DataContext="{StaticResource MainViewModel}">
        
        <Grid.RowDefinitions>
            <RowDefinition Height="32"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal" Background="#333">
            <CheckBox Margin="5" Foreground="#FFF" Content="Enable Zoom?" IsChecked="{Binding EnableZoom, Mode=TwoWay}"/>
            <CheckBox Margin="5" Foreground="#FFF" Content="Enable Pan?" IsChecked="{Binding EnablePan, Mode=TwoWay}" />
        </StackPanel>
        <!-- New code here. Add ItemsControl bound to ChartPanes -->
        <ItemsControl ItemsSource="{Binding ChartPanes}" Grid.Row="1">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid Rows="2">
                    </UniformGrid>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <!-- Don't forget to add Grid.Row=1 -->
                    <s:SciChartSurface
                                       RenderableSeries="{s:SeriesBinding RenderableSeries}" 
                                       Annotations="{s:AnnotationsBinding Annotations}"
                                       YAxes="{s:AxesBinding YAxes}"
                                       XAxes="{s:AxesBinding XAxes}"
                                       ChartTitle="{Binding ChartTitle}">
                        <s:SciChartSurface.ChartModifier>

                            <!-- Bind ModifierGroup using MouseManager.MouseEventGroup attached property -->
                            <s:ModifierGroup s:MouseManager.MouseEventGroup="{Binding MouseEventGroup}">

                                <s:RubberBandXyZoomModifier IsEnabled="{Binding Path=DataContext.EnableZoom, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}" />
                                <s:ZoomPanModifier IsEnabled="{Binding Path=DataContext.EnablePan, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}" ClipModeX="None" />
                                <s:ZoomExtentsModifier/>
                                <s:LegendModifier ShowLegend="True" Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Center"/>
                                <s:RolloverModifier ShowTooltipOn="MouseHover"/>
                            </s:ModifierGroup>
                        </s:SciChartSurface.ChartModifier>
                    </s:SciChartSurface>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

 

In the above code, we declare an ItemsControl in the MainWindow.xaml. this is bound to a collection of ChartViewModel exposed by MainViewModel.

Each ChartViewModel has a collection of RenderableSeries, Annotations and provides the data and parameters for a chart. We have two of them arranged vertically on the view.

Finally, we link mouse events between the two charts by using the MouseManager.MouseEventGroup attached property. This is bound to a MouseEventGroup property in ChartViewModel. When two or more charts share the same string ID, they will share mouse events such as cursor, zooming and panning.