RSS Feed
News
Apr
25
Create MultiPane Stock Charts with SciChartGroup
Posted by Andrew on 25 April 2014 06:19 PM

The following is a preview of SciChartGroup – a hidden feature in SciChart, which is being promoted to first-class citizen as of SciChart v3.0. In the upcoming SciChart v3.0 release there will be an example showing you how to create multi-pane stock charts using SciChartGroup and SciStockChart so you can create awesome WPF trading systems using SciChart.

Of course, being SciChart based its fast! We append over 10 years of daily EUR USD data plus the MACD, RSI and Volume in child panes and zooming, panning is completely lag free.

We intend to add to this API over the coming releases as well as some other exciting improvements, but we want to hear your feedback! If you like what you see, or want to contribute ideas, please contact us. We are always happy to hear your feedback!

 


Read more »



Feb
18
Screenshots, XPS Printing, X-Axis Text Labels
Posted by Andrew on 18 February 2013 09:35 PM

Screenshots, XPS Printing, X-Axis Text Labels

I can’t recall the number of questions we’ve received on whether SciChart supports render to bitmap (screenshots), printing to XPS or PDF and text labels on X-Axis, so we’ve created a tutorial which encompasses all three! Hurrah!

Fortunately both Render-to-Bitmap and XPS printing are both features native to WPF. We present a demonstration application which does these, plus shows a few other neat tricks to get X-Axis text labels on the chart (instead of numeric data).

So here it is! Let’s get started. You will need SciChart v1.5.x for this.

Downloading the Accompanying Source Code

In many of our tutorials, we walk you through how to create it from the ground up. In this we’re going to go a little differently. We will talk you through the code so you can discover how we did screenshots and XPS export.

First download the source code, you can find i.e. here: ScreenshotsXpsAndXLabels.zip

If you don’t have the SciChart trial you will need to download this also and ensure Abt.Controls.SciChart.Wpf.dll is referenced.

Next, running the application, you should see this output:

 

The Screenshots, XPS Printing and XAxis Labels Test App

The Screenshots, XPS Printing and XAxis Labels Test App

 

Drawing the Chart – ChartView.xaml

Let’s begin to break this application down. The ChartView / ChartViewModel defines the XAML and data to render the chart. Here is the source for ChartView.xaml:

 

<UserControl x:Class="ScreenshotsAndXpsExport.ChartView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:s="clr-namespace:Abt.Controls.SciChart;assembly=Abt.Controls.SciChart.Wpf"
             xmlns:ScreenshotsAndXpsExport="clr-namespace:ScreenshotsAndXpsExport"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.Resources>
        <ScreenshotsAndXpsExport:ChartViewModel x:Key="viewModel"/>
    </UserControl.Resources>

    <Grid DataContext="{StaticResource viewModel}">

        <s:SciChartSurface x:Name="sciChart" s:ThemeManager.Theme="BlackSteel" 
                           ChartTitle="Developer Awesomeness vs. Programming Language"
                           DataSet="{Binding ChartData}">

            <s:SciChartSurface.RenderableSeries>
                <s:FastColumnRenderableSeries SeriesColor="#FF6600" FillColor="#33FF6600" DataPointWidth="0.4" />
            </s:SciChartSurface.RenderableSeries>

            <s:SciChartSurface.YAxis>
                <s:NumericAxis VisibleRange="0,10" AxisTitle="Awesomeness" AxisAlignment="Left" />
            </s:SciChartSurface.YAxis>

            <s:SciChartSurface.XAxis>
                <s:NumericAxis VisibleRange="-0.5,4.5" AxisTitle="Programming Language" AutoTicks="False" 
                               MajorDelta="1" MinorDelta="0.2"
                               LabelFormatter="{Binding XLabelFormatter}"/>
            </s:SciChartSurface.XAxis>

            <s:SciChartSurface.ChartModifier>
                <s:CursorModifier/>
            </s:SciChartSurface.ChartModifier>

        </s:SciChartSurface>

    </Grid>
</UserControl>

There’s nothing special here. The SciChartSurface contains a single RenderableSeries (column series), and the YAxis and XAxis are of type NumericAxis. The ScIChartSurface has a single binding of DataSet to ChartData on the ChartViewModel and the DataContext of the control is set to an instance of the ChartViewModel.

There is a single ChartModifier – in this case a CursorModifier.

Oh wait, I’m wrong! There is something special here. Notice that the XAxis has a binding to LabelFormatter:

<s:SciChartSurface.XAxis>
    <s:NumericAxis VisibleRange="-0.5,4.5" AxisTitle="Programming Language" AutoTicks="False" 
                    MajorDelta="1" MinorDelta="0.2"
                    LabelFormatter="{Binding XLabelFormatter}"/>
</s:SciChartSurface.XAxis>

We will discuss what is a LabelFormatter later, but please make a mental note, we are setting AutoTicks=false, MajorDelta = 1, MinorDelta = 0.2 and binding to ChartViewModel.XLabelFomatter.

Drawing the Chart – ChartViewModel.cs

Let’s have a look at the code for ChartViewModel.cs

public class ChartViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private IDataSeriesSet _chartData;
    private ILabelFormatter _labelFormatter;

    public ChartViewModel()
    {
        var dataset = new DataSeriesSet<int, double>();
        var dataSeries = dataset.AddSeries();

        // Create the X-Axis Labels
        var xAxisLabels = new[] {"Java", "Obj-C", "C#", "Fortran", "C++"};

        // We append Y-values (double) and X-values are just indices to the x-axis (string) labels
        dataSeries.Append(Enumerable.Range(0, 5), new[] { 2.2, 5.3, 9.5, 6.7, 8.4});
        ChartData = dataset;

        // The ILabelFormatter allows us to substitute a string value for a data-value. 
        // In most cases this is used to format a numeric value, e.g. 1.23456 as "1.23", however
        // we can also use it to transform an integer value to a string (text) value
        XLabelFormatter = new CustomLabelFormatter(xAxisLabels);
    }

    public IDataSeriesSet ChartData
    {
        get { return _chartData; }
        set
        {
            _chartData = value;
            OnPropertyChanged("ChartData");
        }
    }

    public ILabelFormatter XLabelFormatter
    {
        get { return _labelFormatter; }
        set
        {
            _labelFormatter = value;
            OnPropertyChanged("XLabelFormatter");
        }
    }

    protected virtual void OnPropertyChanged(string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

Some of this is standard and well known to you by now. For instance, we create a DataSeriesSet with int, double types, append some X,Y values and implement INotifyPropertyChanged.

What’s new is we are creating an instance of CustomLabelFormatter. This is a class which implements ILabelFormatter, a new type introduced in ScIChart v1.5. This allows us to substitute integer values on the X-Axis for string labels. If you recall from our screenshot of Programming Language of Developer Awesomeness, the X-Axis had text labels:

Developer Awesomeness labels provided by ILabelFormatter

Developer Awesomeness labels provided by ILabelFormatter

 

Using ILabelFormatter to Format Axis and Cursor Labels

So how do you use this amazing new feature to format axis labels I hear you say? It’s really simple. Create a class which implements ILabelFormatter and attach (via binding or code) to SciChartSurface.XAxis.LabelFormatter.

Here’s the code for CustomLabelFormatter:

public class CustomLabelFormatter : ILabelFormatter
{
    private readonly string[] _xLabels;

    public CustomLabelFormatter(string[] xLabels)
    {
        _xLabels = xLabels;
    }

    /// <summary>
    /// Called when the label formatted is initialized as it is attached to the parent axis, with the parent axis instance
    /// </summary>
    /// <param name="parentAxis">The parent <see cref="T:Abt.Controls.SciChart.IAxis" /> instance</param>
    public void Init(IAxis parentAxis)
    {
    }

    /// <summary>
    /// Called at the start of an axis render pass, before any labels are formatted for the current draw operation
    /// </summary>
    public void OnBeginAxisDraw()
    {
    }

    /// <summary>
    /// Formats a label for the axis from the specified data-value passed in
    /// </summary>
    /// <param name="dataValue">The data-value to format</param>
    /// <returns>
    /// The formatted label string
    /// </returns>
    /// <exception cref="System.NotImplementedException"></exception>
    public string FormatLabel(IComparable dataValue)
    {
        int index = (int) Convert.ChangeType(dataValue, typeof (int));
        if (index >= 0 && index < _xLabels.Length)
            return _xLabels[index];

        return index.ToString();
    }

    /// <summary>
    /// Formats a label for the cursor, from the specified data-value passed in
    /// </summary>
    /// <param name="dataValue">The data-value to format</param>
    /// <returns>
    /// The formatted cursor label string
    /// </returns>
    /// <exception cref="System.NotImplementedException"></exception>
    public string FormatCursorLabel(IComparable dataValue)
    {
        int index = (int)Convert.ChangeType(dataValue, typeof(int));
        if (index >= 0 && index < _xLabels.Length)
            return _xLabels[index];

        return index.ToString();
    }
}

The method Init() is called when the LabelFormatter is attached to an axis. Use this method if you need a reference to the axis you are formatting.

OnBeginAxisDraw() is called at the start of an axis render, before any labels are formatted for the current draw operation. You can use this to reset any state per-draw.

FormatLabel() and FormatCursorLabel() are used to transform a data-value into a string label for Axis and Cursor respectively. In this example we simply convert the DataValue to an integer, and index a string array of labels.

Note that the CustomLabelFormatter is created in the ChartViewModel with this code:

// Create the X-Axis Labels
var xAxisLabels = new[] {"Java", "Obj-C", "C#", "Fortran", "C++"};

// ...

// The ILabelFormatter allows us to substitute a string value for a data-value. 
// In most cases this is used to format a numeric value, e.g. 1.23456 as "1.23", however
// we can also use it to transform an integer value to a string (text) value
XLabelFormatter = new CustomLabelFormatter(xAxisLabels);

So we’re literally taking data-values 0,1,2,3,4 as indexes to a string array of labels.

Adding Screenshots – MainView.xaml, MainView.xaml.cs

Next take a look at MainView.xaml and MainView.xaml.cs. We couldn’t be bothered to create a MainViewModel (naughty), but this code demonstrates the point.

Here’s the XAML for MainView:

<Window x:Class="ScreenshotsAndXpsExport.MainWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:ScreenshotsAndXpsExport" 
             mc:Ignorable="d" 
             d:DesignHeight="480" d:DesignWidth="640">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="32"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal" Background="#1D2C35">   
            <Button Content="Copy to Clipboard" Margin="3" Click="CopyToClipboardClick"></Button>
            <Button Content="Save to PNG" Margin="3" Click="SaveAsPngClick"></Button>
            <Button Content="Print to XPS" Margin="3" Click="PrintToXpsClick"></Button>
            <TextBlock Foreground="#EEE" Margin="8" Text="Or Double-click chart to take screenshot with cursor"></TextBlock>
        </StackPanel>

        <local:ChartView Grid.Row="1" x:Name="customUserControl" IsAnimatedOnLoad="True"
                                 MouseDoubleClick="CustomUserControl_OnMouseDoubleClick"/>
    </Grid>
</Window>

This adds a few buttons (toolbar) above the ScIChartSurface in ChartView. These buttons will be used to take screenshots and print to XPS.

We always knew C# Developers were awesome, and now with SciChart we can prove it!

We always knew C# Developers were awesome, and now with SciChart we can prove it!

Let’s break down the code in MainView.xaml.cs to see how we render to bitmap and print to XPS:

To Render to Bitmap on Clipboard

… we call an extension method, ExportBitmapToClipboard(). See SciChartSurfaceExtensions.cs below for a definition of this method

private void CopyToClipboardClick(object sender, RoutedEventArgs e)
{
    // See SciChartSurfaceExtensions where ExportBitmapToClipboard is defined
    this.customUserControl.SciChartSurface.ExportBitmapToClipboard();

    MessageBox.Show("Copied to Clipboard!");
}

To Render to Bitmap and Save PNG

… we call an extension method, ExportBitmapToFile(). See SciChartSurfaceExtensions.cs below for a definition of this method.

private void SaveAsPngClick(object sender, RoutedEventArgs e)
{
    var dialog = new SaveFileDialog();
    dialog.DefaultExt = "png";
    dialog.AddExtension = true;
    dialog.Filter = "Png Files|*.png";
    dialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
    if (dialog.ShowDialog() == true)
    {
        // See SciChartSurfaceExtensions where ExportBitmapToFile is defined
        this.customUserControl.SciChartSurface.ExportBitmapToFile(dialog.FileName);
        Process.Start(dialog.FileName);
    }
}

To Print to XPS

… we demonstrate something slightly different here. First of all you could print the chart already shown, but what if you wanted to size the chart according to print dimensions and export to XPS or PDF, along with some other text in a report? Or, what if you wanted to export the chart as a screenshot but before showing it, e.g. on server-side code? This method demonstrates how we can do this:

private void PrintToXpsClick(object sender, RoutedEventArgs e)
{
    var dialog = new PrintDialog();
    if (dialog.ShowDialog() == true)
    {
        var size = new Size(dialog.PrintableAreaWidth, dialog.PrintableAreaWidth*3/4);

        var scs = CreateSciChartSurfaceWithoutShowingIt(size);

        // And print. This works particularly well to XPS! 
        Action printAction = () => dialog.PrintVisual(scs, "Programmer Awesomeness");
        Dispatcher.BeginInvoke(printAction);
    }
}

First, use PrintDialog to present the dialog to theuser. This also lets us get the PrintableAreaWidth/PrintableAreaHeight. We want to print our chart at full width but at a 4:3 ratio.

Next, the method CreateSciChartSurfaceWithoutShowingIt() is used to instantiate a new ChartView/ChartViewModel but without adding to the visual tree.

/// <summary>
/// This method demonstrates how we can render to bitmap or print a SciChartSurface which is never shown, 
/// e.g. if you wanted to create a report on a server, or wanted to grab a screenshot at a different resolution
/// to the currently shown size of the chart
/// </summary>
/// <param name="size"></param>
/// <returns></returns>
private Visual CreateSciChartSurfaceWithoutShowingIt(Size size)
{
    // Create a fresh ChartView, this contains the ViewModel (declared in XAML) and data
    var control = new ChartView();
    var scs = control.SciChartSurface;

    // RenderPriority.Immediate is required to print or render to bitmap any SciChartSurface that has not been 
    // added to the Visual Tree. This ensures that re-draw events are processed synchronously
    //
    // Note RenderPriority.Immediate should never be used for real-time charts!! 
    scs.RenderPriority = RenderPriority.Immediate;

    // ApplyTemplate is required on the newly created chart to ensure the theme is applied
    scs.ApplyTemplate();

    // Set the width/height explicitly based on print area
    scs.Width = size.Width;
    scs.Height = size.Height;

    // Force the Loaded event for the SciChartSurface
    scs.OnLoad();

    // Now measure & arrange
    scs.Measure(new Size(scs.Width, scs.Height));
    scs.Arrange(new Rect(new Point(0, 0), scs.DesiredSize));
    scs.UpdateLayout();

    // Finally trigger a redraw
    scs.InvalidateElement();

    return scs;
}

The important points to note here are, if you want to render to bitmap or print a SciChartSurface that has not yet been shown or added to the Visual Tree, you must:

  • Create the chart, ensure DataSet, XAxis, YAxis, RenderableSeries have been set.
  • Ensure Themes (optional) have been applied.
  • Ensure SciChartSurface.RenderPriority = RenderPriority.Immediate has been set. This prevents the render-throttling which is very useful in a real-time chart, but prevents rendering if the chart is not visible.
  • Ensure SciChartSurface.ApplyTemplate() has been called to force themes (even the default) to load.
  • Ensure SciChartSurface.OnLoad() has been called, to force loading / initialization.
  • Ensure Measure, Arrange, UpdateLayout have been called to force the correct size of the chart.
  • Ensure SciChartSurface.InvalidateElement() has been called to render the chart in memory.

At this point you will have a SciChartSurface which is prepared and ready to render to bitmap or print, even though it is not in the Visual Tree.

The final step is we use the PrintDialog to print the chart

// And print. This works particularly well to XPS! 
Action printAction = () => dialog.PrintVisual(scs, "Programmer Awesomeness");
Dispatcher.BeginInvoke(printAction);

SciChartSurfaceExtensions.cs – Where the Screenshot Magic Happens

The following class exposes two extension methods which may be used to render a SciChartSurface to a bitmap or PNG file. These use purely WPF techniques but apply some tricks (Measure, Arrange, Layout) to ensure the SciChartSurface is ready to render to bitmap.

 

public static class SciChartSurfaceExtensions
{
    public static void ExportBitmapToClipboard(this SciChartSurface element)
    {
        var rtb = ExportToBitmap(element);
        Clipboard.SetImage(rtb);
    }

    public static void ExportBitmapToFile(this SciChartSurface element, string filename)
    {
        using (var filestream = new FileStream(filename, FileMode.Create))
        {
            var encoder = new PngBitmapEncoder();
            var bitmap = ExportToBitmap(element);
            encoder.Frames.Add(BitmapFrame.Create(bitmap));
            encoder.Save(filestream);
            filestream.Close();
        }
    }

    public static BitmapSource ExportToBitmap(this SciChartSurface element)
    {
        if (element == null)
            return null;

        // Store the Frameworks current layout transform, as this will be restored later
        var storedTransform = element.LayoutTransform;

        // Set the layout transform to unity to get the nominal width and height
        element.LayoutTransform = new ScaleTransform(1, 1);

        element.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
        element.Arrange(new Rect(new Point(0, 0), element.DesiredSize));

        var height = element.ActualHeight + element.Margin.Top + element.Margin.Bottom;
        var width = element.ActualWidth + element.Margin.Left + element.Margin.Right;

        // Render to a Bitmap Source, note that the DPI is
        // changed for the render target as a way of scaling the FrameworkElement
        var rtb = new RenderTargetBitmap(
                (int)width,
                (int)height,
                96d,
                96d,
                PixelFormats.Default);

        // Render a white background in Clipboard
        var vRect = new Rectangle
        {
            Width = width,
            Height = height,
            Fill = Brushes.White
        };
        vRect.Measure(element.RenderSize);
        vRect.Arrange(new Rect(element.RenderSize));
        rtb.Render(vRect);
        rtb.Render(element);

        // Restore the Framework Element to it’s previous state
        element.LayoutTransform = storedTransform;
        element.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
        element.Arrange(new Rect(new Point(0, 0), element.DesiredSize));

        return rtb;
    }
}

Running the Application

Ok so now you’ve done the walkthrough, if you haven’t already, I invite you to test the application by capturing screenshots, saving PNG files and printing. Printing to XPS is particularly useful, as this is a Vector format which may be scaled, zoomed or converted to PDF with no loss of quality.

 

Try Printing to XPS - it really works!

Try Printing to XPS – it really works!

 

Note that it will become apparent which parts of SciChart are vector (the axis, labels, gridlines, chart titles, cursors) and which are rasterized (the *RenderableSeries) as the series won’t scale as well. Still, you have a method above to render to the desired resolution which should mitigate this somewhat!

XPS is a Vector Format, so zooms with no loss of quality. Note that series are Rasterized to bitmaps by SciChart’s high speed drawing engine so are not in Vector format.

XPS is a Vector Format, so zooms with no loss of quality. Note that series are Rasterized to bitmaps by SciChart’s high speed drawing engine so are not in Vector format.

So, we hope that clears up the matter of how to render SciChart to bitmaps, or print! If you have any questions or feedback, we’d love to hear it in the comments below.

Thanks and enjoy!


Read more »



Feb
16
Databinding Annotations with MVVM
Posted by admin on 16 February 2013 04:46 PM

Annotations with MVVM

An often requested demonstration is how to add and remove annotations on a SciChartSurface using the MVVM pattern. MVVM is a great way to split your code into View (user interface) and business logic, to create decoupled, testable and maintainable applications.

It’s easy to forget sometimes the purpose of MVVM (or any pattern) is to simplify software development and decouple modules, making for more maintainable software, when sometimes we do exactly the opposite in order to meet a rule! This article will demonstrate a way we can bind annotations to viewmodels without compromising MVVM, or introducing any nasty hacks just to make it happen.

So here it is! Let’s get started. You will need to download and install SciChart v1.5.x for this.

Creating the Solution

First lets start by creating a Visual Studio solution. Here at SciChart we use VS2012, however VS2010 is also supported.

Create a new WPF project in your chosen IDE and add a reference to SciChart WPF. You can find the libraries under C:/Program Files (x86)/ABT Software Services/SciChart/Lib/

Referencing SciChart WPF from Program Files

Now with the solution created, add a SciChartSurface to the MainWindow.xaml. It should build at this point. If not, double check your references, the target .NET framework and xmlns namespace declarations.

Creating the SciChartSurface in Xaml. It should look like this!

Now lets add some Axes to the chart. We will assign a VisibleRange and GrowBy properties, plus assign a theme to cause the chart to paint itself in the designer.

<Window x:Class="AnnotationsMvvm.MainWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:s="clr-namespace:Abt.Controls.SciChart;assembly=Abt.Controls.SciChart.Wpf" 
             xmlns:local="clr-namespace:AnnotationsMvvm" 
             mc:Ignorable="d" 
             d:DesignHeight="480" d:DesignWidth="640">

    <Grid>

      <!-- Note, we're using the new MVVM SeriesSource API in v1.5 -->
      <s:SciChartSurface SeriesSource="{Binding ChartSeries}" s:ThemeManager.Theme="Chrome">

        <s:SciChartSurface.XAxis>
          <s:NumericAxis VisibleRange="0,100"/>
        </s:SciChartSurface.XAxis>

        <s:SciChartSurface.YAxis>
          <s:NumericAxis VisibleRange="-5,5"/>
        </s:SciChartSurface.YAxis>

      </s:SciChartSurface>
    </Grid>
</Window>

You should now be looking at something a little like this:

After adding Axes to the Chart, it should look like this in the designer

Adding a MainViewModel

The MainViewModel is going to be the controller / data-provider for the MainWindow in this example. It is going to provide chart data, series colors and data-objects for the annotations.

Here’s the code for the MainViewModel in entirety. In a moment we will walk through the code to explain it.

namespace AnnotationsMvvm
{
    public class MainViewModel : INotifyPropertyChanged
    {
        private ObservableCollection<IChartSeriesViewModel> _chartSeries;
        private IEnumerable<LabelViewModel> _chartLabels;

        // I'll leave it to you to wire up PropertyChanged to your base viewmodel type
        public event PropertyChangedEventHandler PropertyChanged;

        public MainViewModel()
        {
            var ds0 = new XyDataSeries<double, double>();

            // RandomWalkGenerator is found in the examples source code
            var someData = new RandomWalkGenerator().GetRandomWalkSeries(100); 

            ds0.Append(someData.XData, someData.YData);

            // With SeriesSource API you do not need a DataSet. This is created automatically to match
            // the type of DataSeries<Tx,Ty>. All DataSeries must be the same type however

            // Create a list of ChartSeriesViewModel which unify dataseries and render series
            // This way you can add/remove your series and change the render series type easily
            // without worrying about DataSeriesIndex as per SciChart v1.3
            _chartSeries = new ObservableCollection<IChartSeriesViewModel>();
            _chartSeries.Add(new ChartSeriesViewModel(ds0, new FastLineRenderableSeries()));

            // Now create the labels
            _chartLabels = new[]
                               {
                                   new LabelViewModel(5, -2.5, "Label0", "Label0 Tooltip!"), 
                                   new LabelViewModel(20, -2, "Label1", "Label1 Tooltip!"), 
                                   new LabelViewModel(35, 3, "Label2", "Label2 Tooltip!"), 
                                   new LabelViewModel(50, 1.5, "Label3", "Label3 Tooltip!"), 
                                   new LabelViewModel(65, -0.5, "Label4", "Label4 Tooltip!"), 
                               };
        }

        public ObservableCollection<IChartSeriesViewModel> ChartSeries { get { return _chartSeries; }}

        public IEnumerable<LabelViewModel> ChartLabels { get { return _chartLabels; } }
    }
}

 

Breaking this down, what we do is first create an XyDataSeries. This stores X-Y values in an optimized data structure that SciChart can consume. We append a Random Walk (randomly generated time series).

 

var ds0 = new XyDataSeries<double, double>();

// RandomWalkGenerator is found in the examples source code
var someData = new RandomWalkGenerator().GetRandomWalkSeries(100); 

ds0.Append(someData.XData, someData.YData);

You should note the XyDataSeries expects data to be sorted in the X-Direction. This is to enable fast indexing of the data when zooming and panning. If you want to add unsorted data-values then you should check out the UnsortedXyDataSeries.

Also note that the RandomWalkGenerator class can be found in the examples source code contained in the SciChart Trial Installer.

The next part we create a collection of ChartSeriesViewModels. SciChart can bind to these in an MVVM context from the SeriesSource property

 

// With SeriesSource API you do not need a DataSet. This is created automatically to match
// the type of DataSeries<Tx,Ty>. All DataSeries must be the same type however

// Create a list of ChartSeriesViewModel which unify dataseries and render series
// This way you can add/remove your series and change the render series type easily
// without worrying about DataSeriesIndex as per SciChart v1.3
_chartSeries = new ObservableCollection<IChartSeriesViewModel>();
_chartSeries.Add(new ChartSeriesViewModel(ds0, new FastLineRenderableSeries()));

 

In the above code we create an ObservableCollection of IChartSeriesViewModel. This is a viewmodel type defined in the SciChart library, which unifies a DataSeries and RenderableSeries. The DataSeries has already been filled with data. I won’t go too much into detail of the SeriesSource API, save to say there are plenty of MVVM demos on our website which show how to use this API.

 

Creating Annotation ViewModels

This next part is interesting. Here we define and expose an ObservableCollection of annotation view models.

 

// Inside MainViewModel, we now create the labels
_chartLabels = new[]
{
	new LabelViewModel(5, -2.5, "Label0", "Label0 Tooltip!"), 
	new LabelViewModel(20, -2, "Label1", "Label1 Tooltip!"), 
	new LabelViewModel(35, 3, "Label2", "Label2 Tooltip!"), 
	new LabelViewModel(50, 1.5, "Label3", "Label3 Tooltip!"), 
	new LabelViewModel(65, -0.5, "Label4", "Label4 Tooltip!"), 
};
// ...
public IEnumerable<LabelViewModel> ChartLabels { get { return _chartLabels; } }

 

Where is LabelViewModel defined? This is a custom class created for this tutorial. Basically you just need to create a class, ensure it implements INotifyPropertyChanged, and has some properties to define the position and labels. Here is the LabelViewModel class in entirety:

 

public class LabelViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public LabelViewModel(IComparable x1, IComparable y1, string text, string tooltip)
    {
        X1 = x1;
        Y1 = y1;
        LabelText = text;
        LabelToolTip = tooltip;
    }

    /// <summary>
    /// LabelText will allow us to bind to TextAnnotation.Text
    /// </summary>
    public string LabelText { get; set; }

    /// <summary>
    /// LabelTooltip will be used to add a custom tooltip to the TextAnnotation
    /// </summary>
    public string LabelToolTip { get; set; }

    /// <summary>
    /// X1 defines the X Data-Value for positioning the annotation
    /// </summary>
    public IComparable X1 { get; set; }

    /// <summary>
    /// Y1 defines the Y Data-Value for positioning the annotation
    /// </summary>
    public IComparable Y1 { get; set; }
}

Running the Application so far

Ok, so to run the example and see some data in the chart, we need to ensure an instance of the MainViewModel is set on the chart. Add this XAML to the top of your MainWindow.xaml file.

<Window x:Class="AnnotationsMvvm.MainWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:s="clr-namespace:Abt.Controls.SciChart;assembly=Abt.Controls.SciChart.Wpf" 
             xmlns:local="clr-namespace:AnnotationsMvvm" 
             mc:Ignorable="d" 
             d:DesignHeight="480" d:DesignWidth="640">

    <Window.Resources>
        <local:MainViewModel x:Key="viewModel"/>
    </Window.Resources>

    <Grid DataContext="{StaticResource viewModel}">

      <s:SciChartSurface SeriesSource="{Binding ChartSeries}" 
              <!-- Omitted for brevity ... -->
      </s:SciChartSurface>
    </Grid>
</Window>

 

Now, if you run the application so far, you should see something like this:

How the Application Looks now - we should see a Line Chart

How the Application Looks now – we should see a Line Chart

You should even see the line chart in the designer view!

You should even see the line chart in the designer view!

 

You should even see the data in the designer, because the ViewModel bindings are evaluated at design time. Isn’t that neat!?


Don’t see it? Here are some tips to debug why the chart is not showing:

  • Did you add X and Y Axes to the chart? VisibleRange and GrowBy are not essential properties, but the chart won’t redraw without axes
  • Did you bind to the ViewModel and to the SeriesSource property?
  • Did you fill the DataSeries with data and add a FastLineRenderableSeries with SeriesColor to the ChartSeriesViewModel?
  • Are there any binding errors in the output window of Visual Studio? If so, what?
  • Still stuck? Contact us and tell us to improve our quality of tutorials and debug error messages! We’d love to hear from you!

Binding to LabelViewModels by Creating a Custom ChartModifier

If you recall we instantiated a collection of LabelViewModels in the MainViewModel. We need to bind to these and add them to the chart. To do this we can create a custom ChartModifier.

SciChart exposes a rich API for interacting with the chart data or visible ranges. By inheriting from ChartModifierBase, you can access API functions for manipulating range, responding to mouse events etc…

Let’s get started. Create a class which inherits ChartModifierBase. We are going to add a DependnecyProperty called LabelsSource, of type IEnumerable. We will later bind this directly to the MainViewModel.Labels property

 

public class CustomAnnotationChartModifier : ChartModifierBase
{
    public static readonly DependencyProperty LabelsSourceProperty = DependencyProperty.Register("LabelsSource", typeof(IEnumerable), typeof(CustomAnnotationChartModifier), new PropertyMetadata(null, OnLabelsSourceChanged));

    // Here LabelsSource is IEnumerable, but you could easily make it 
    // ObservableCollection<LabelViewModel> 
    // in order to get changed notifications when items are added to, or removed from the collection
    public IEnumerable LabelsSource
    {
        get { return (IEnumerable) GetValue(LabelsSourceProperty); }
        set { SetValue(LabelsSourceProperty, value); }
    }

    // Get a notification when new labels are set.
    private static void OnLabelsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var modifier = (CustomAnnotationChartModifier) d;
        IEnumerable newValue = e.NewValue as IEnumerable;
        if (newValue == null) return;

        modifier.RebuildAnnotations();
    }

    // Recreate all annotations
    private void RebuildAnnotations()
    {
       // Todo
    }

    /// <summary>
    /// Called when the Chart Modifier is attached to the Chart Surface
    /// </summary>
    public override void OnAttached()
    {
        base.OnAttached();

        // Catch the condition where LabelsSource binds before chart is shown. Rebuild annotations
        if (LabelsSource != null && base.ParentSurface.Annotations.Count == 0)
        { 
            RebuildAnnotations();
        }
    }
}

 

Ok so far, this is really simple. Now you can add the CustomAnnotationChartModifier to the ScIChartSurface. You can also use this alongside other chart modifiers, e.g. ZoomPanModifier, RubberBandXyZoomModifier, ZoomExtentsModifier if placed in a modifier group. We’re going to start off by just adding the single modifier and declaring the binding to LabelsSource property in the MainViewModel:

 

<s:SciChartSurface SeriesSource="{Binding ChartSeries}" 

	<!-- Omitted for brevity ... -->

	<s:SciChartSurface.ChartModifier>
	  <local:CustomAnnotationChartModifier LabelsSource="{Binding ChartLabels}"/>
	</s:SciChartSurface.ChartModifier>

</s:SciChartSurface>

 

If you run the app now, you should be able to put a breakpoint in the CustomAnnotationChartModifier.OnLabelsSourceChanged method and step through to see if the binding is wired up correctly.

What next? Lets add and position our annotations. Remember the RebuildAnnotations() method with its //TODO comment? Ok, now lets add this body to the chart.

 

// Recreate all annotations, called when LabelsSource property changes
// or when the CustomAnnotationChartModifier is attached to the parent surface
private void RebuildAnnotations()
{
    if (base.ParentSurface == null || LabelsSource == null)
        return;

    // Take a look at the base class, ChartModifierBase for a wealth of API 
    // methods and properties to manipulate the SciChartSurface
    var annotationCollection = base.ParentSurface.Annotations;
    annotationCollection.Clear();

    foreach(var item in LabelsSource)
    {
        annotationCollection.Add(new CustomTextAnnotation() { DataContext = item });
    }
}

 

We also need a custom annotation. All this is, is a class that inherits CustomTextAnnotation but has the bindings to LabelText and LabelTooltip. There are other ways to do this, for instance:

  • You could create a TextAnnotation here and do the bindings in code
  • You could define the TextAnnotation in the element of the ChartModifier, or SciChartSurface, or even parent control and access it by using FrameworkElement.TryFindResource()

Anyway, for simplicity we’re going to create a class which inherits TextAnnotation and define the bindings in there. Here’s the source:

 

<s:TextAnnotation x:Class="AnnotationsMvvm.CustomTextAnnotation"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:s="clr-namespace:Abt.Controls.SciChart;assembly=Abt.Controls.SciChart.Wpf" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             Text="{Binding LabelText}"
             Foreground="#333"
             Background="#99FFFFFF"
             BorderBrush="#77333333"
             BorderThickness="1"
             CornerRadius="2"
             VerticalAnchorPoint="Center"
             HorizontalAnchorPoint="Center"
             X1="{Binding X1}"
             Y1="{Binding Y1}">
  <ToolTipService.ToolTip>
    <Grid>
      <TextBlock Text="{Binding LabelToolTip}"/>
    </Grid>
  </ToolTipService.ToolTip>
</s:TextAnnotation>

 

The important parts are:

  • The Text property binding Text=”{Binding LabelText}”
  • The X1 and Y1 property bindings X1=”{Binding X1}”
  • The Tooltip binding <TextBlock Text=”{Binding LabelToolTip}”/>

Ok, now running the app, you should see something like this

 

The completed tutorial - Label position, text and tooltip are all data-bound to LabelViewModels inside your MainViewModel

The completed tutorial – Label position, text and tooltip are all data-bound to LabelViewModels inside your MainViewModel

 

Tah dah!

Finally if you hover the labels you will see they have a tooltip, data-bound to the LabelViewModel.

What to do if it doesn’t work

Again check you don’t have any binding errors in the output window. Are your series rendering? If not you will need to ensure SeriesSource binding is being evaluated properly, that you have bound to a property with DataSeries and RenderableSeries and have declared an X and Y axes of the correct type on the SciChartSurface.

If your labels are not showing, ensure you have created and bound to a LabelViewModel collection by checking for binding errors, and by putting a breakpoint in your CustomAnnotationChartModifier.RebuildAnnotations method. Finally double check you have bound the properties on CustomTextAnnotation to the LabelViewModel properties.

So, MVVM Text Annotations, Great! So what else can be data-bound?

In this example we’ve demonstrated how to bind TextAnnotation position, label and tooltip to ViewModels. So what else can you do?

Well,  In this example we data-bind trades (buy, sell) and news bullets on a price chart by binding the ScIChartSurface.Annotations property directly to an AnnotationCollection property in the ViewModel.

You could also data-bind the Y1 value of a HorizontalLineAnnotation to create a dynamic threshold, either editable from the UI or set programmatically from the ViewModel.

Or maybe you want to databind lines, or arrows created by the user (via clicks) in the view-model. In this case you would use the same technique as the Trade Markers example, by binding to AnnotationCollection in the ViewModel, after the user creates annotations you can access the newly created annotation position.

Executable and downloadable

For your convenience, we’ve also included the Annotations With MVVM Tutorial source code here. however note it doens’t have the SciChart binaries, you’ll need to get those off the downloads page.

Thanks and enjoy!

Screen Shot 2013-02-16 at 16.44.02


Read more »



Jan
8
Synchronizing ChartModifier Mouse Events Across Charts
Posted by Andrew on 08 January 2013 10:26 PM

Multi Chart Mouse Events

An often requested demonstration is how to synchronize mouse events across charts. We have used this feature in the SciTrader demo that you can view online (Silverlight 5), but what’s lacking is a clear and consise tutorial on this topic without all the other features that SciTrader demonstrates.

So here it is! Let’s get started. You will need SciChart v1.5.x for this.

Creating the Solution

First lets start by creating a Visual Studio solution. Here at SciChart we use VS2012, however VS2010 is also supported.

Create a new WPF project in your chosen IDE and add a reference to SciChart WPF. You can find the libraries under C:/Program Files (x86)/ABT Software Services/SciChart/Lib/

Referencing SciChart WPF from Program Files

Now with the solution created, add a SciChartSurface to the MainWindow.xaml. It should build at this point. If not, double check your references, the target .NET framework and xmlns namespace declarations.

Creating the SciChartSurface in Xaml. It should look like this!

Now lets add some Axes to the chart. We will assign a VisibleRange and GrowBy properties to cause the chart to paint itself in the designer.

<Window x:Class="_08_Synchronize_Mouse_Events.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:s="clr-namespace:Abt.Controls.SciChart;assembly=Abt.Controls.SciChart.Wpf"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <s:SciChartSurface x:Name="chart0" s:ThemeManager.Theme="Chrome" Padding="30">
            <s:SciChartSurface.RenderableSeries>
                <s:FastLineRenderableSeries SeriesColor="DarkBlue"/>               
            </s:SciChartSurface.RenderableSeries>

            <s:SciChartSurface.YAxis>
                <s:NumericAxis VisibleRange="0,1" GrowBy="0.1,0.1"></s:NumericAxis>
            </s:SciChartSurface.YAxis>

            <s:SciChartSurface.XAxis>
                <s:NumericAxis VisibleRange="0,1" GrowBy="0.1,0.1"></s:NumericAxis>
            </s:SciChartSurface.XAxis>

        </s:SciChartSurface>
    </Grid>
</Window>

You should now be looking at something a little like this:

After adding Axes to the Chart, it should look like this in the designer

Adding the ChartModifiers

So the point of this tutorial is to share mouse events across charts, so lets add the ChartModifiers. We’re going to add the following:

  • RolloverModifier – to inspect data points at a vertical location
  • CursorModifier – to inspect data points at X-Y locations
  • ZoomPanModifier – to pan the chart with the mouse
  • RubberBandXyZoomModifier – to zoom the chart by dragging the mouse
  • MouseWheelModifier – to zoom the chart using the mousewheel
  • XAxis and YAxisDragModifiers, to zoom the chart by dragging the axes

Add the following Xaml inside your SciChartSurface tag:

            <s:SciChartSurface.ChartModifier>
                <s:ModifierGroup>
                    <s:RubberBandXyZoomModifier IsEnabled="{Binding ZoomEnabled, Mode=TwoWay}" IsXAxisOnly="True"></s:RubberBandXyZoomModifier>
                    <s:ZoomPanModifier IsEnabled="{Binding PanEnabled, Mode=TwoWay}"></s:ZoomPanModifier>
                    <s:MouseWheelZoomModifier IsEnabled="{Binding MouseWheelEnabled, Mode=TwoWay}"></s:MouseWheelZoomModifier>
                    <s:RolloverModifier IsEnabled="{Binding RolloverEnabled, Mode=TwoWay}"></s:RolloverModifier>
                    <s:CursorModifier IsEnabled="{Binding CursorEnabled, Mode=TwoWay}"></s:CursorModifier>
                    <s:YAxisDragModifier></s:YAxisDragModifier>
                    <s:XAxisDragModifier></s:XAxisDragModifier>
                    <s:ZoomExtentsModifier></s:ZoomExtentsModifier>
                </s:ModifierGroup>
            </s:SciChartSurface.ChartModifier>

This puts the modifiers on the chart inside a ModifierGroup. The behaviour is as follows. The first modifier in the group receives a mouse event (Mouse Down, Up, Move, Wheel, DoubleClick). If it handles it, it prevents modifiers further down the group from receiving the same event (we will see later, a way to override this behaviour). Each modifier attaches specific mouse interaction behaviour to the chart, such as Zooming or Panning, has an IsEnabled property (which may be bound to). You can even create your own modifiers by deriving from ChartModifierBase.

So what now? You will notice we put some bindings in there, so we need a MainViewModel to bind to to enable toggling of the modifiers in unison.

Creating the ViewModel

You don’t have to do this in MVVM, but we recommend it. If you want to enable or disable chart modifiers via code behind, simply give your modifiers an x:Name attribute and set modifier.IsEnabled = true or false.

In MVVM we can bind directly to a Boolean property. This allows us to turn on or off multiple modifiers at once, and bind to other UI such as toggle buttons. Create a viewmodel like this with our modifier properties in. Here we implement INotifyPropertyChanged, but you can also use an MVVM framework such as Mvvm Lite and inherit from their base viewmodel.

    public class MultiChartMouseEventsViewModel : INotifyPropertyChanged
    {
        private bool mouseWheelEnabled;
        private bool panEnabled;
        private bool rolloverEnabled;
        private bool cursorEnabled;
        private bool zoomEnabled;

        public MultiChartMouseEventsViewModel()
        {
            // Set default states of modifiers
            this.MouseWheelEnabled = false;
            this.PanEnabled = true;
            this.CursorEnabled = true;
        }

        public bool MouseWheelEnabled
        {
            get { return mouseWheelEnabled; }
            set
            {
                if (mouseWheelEnabled == value) return;
                mouseWheelEnabled = value;
                OnPropertyChanged("MouseWheelEnabled");
            }
        }

        public bool PanEnabled
        {
            get { return panEnabled; }
            set
            {
                if (panEnabled == value) return;
                panEnabled = value;
                OnPropertyChanged("PanEnabled");

                // Toggle Zoom off
                ZoomEnabled = !PanEnabled;
            }
        }

        public bool ZoomEnabled
        {
            get { return zoomEnabled; }
            set
            {
                if (zoomEnabled == value) return;
                zoomEnabled = value;
                OnPropertyChanged("ZoomEnabled");

                // Toggle pan off
                PanEnabled = !ZoomEnabled;
            }
        }

        public bool CursorEnabled
        {
            get { return cursorEnabled; }
            set
            {
                if (cursorEnabled == value) return;
                cursorEnabled = value;
                OnPropertyChanged("CursorEnabled");

                // Toggle RolloverEnabled off
                RolloverEnabled = !CursorEnabled;
            }
        }

        public bool RolloverEnabled
        {
            get { return rolloverEnabled; }
            set
            {
                if (rolloverEnabled == value) return;
                rolloverEnabled = value;
                OnPropertyChanged("RolloverEnabled");

                // Toggle RolloverEnabled off
                CursorEnabled = !RolloverEnabled;
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

Binding Toolbars to ViewModel Properties

In order to turn the modifiers on and off, lets create a toolbar and bind to viewmodel properties. First, modify the xaml in the MainWindow.xaml to include some rows in the grid:

<Window x:Class="Abt.Controls.SciChart.Wpf.TestSuite.ExampleSandbox.MultiChartMouseEvents.MultiChartMouseEvents"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:s="clr-namespace:Abt.Controls.SciChart;assembly=Abt.Controls.SciChart.Wpf"
        Title="MultiChartMouseEvents" Height="400" Width="600">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="32"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>

        <!-- Define Toolbar -->
        <StackPanel Grid.Row="0" Orientation="Horizontal">
            <ToggleButton Content="Zoom" Margin="3" 
                          IsChecked="{Binding ZoomEnabled, Mode=TwoWay}"/>
            <ToggleButton Content="Pan" Margin="3" 
                          IsChecked="{Binding PanEnabled, Mode=TwoWay}"/>
            <ToggleButton Content="MouseWheel" Margin="3" 
                          IsChecked="{Binding MouseWheelEnabled, Mode=TwoWay}"/>
            <ToggleButton Content="Rollover" Margin="3" 
                          IsChecked="{Binding RolloverEnabled, Mode=TwoWay}"/>
            <ToggleButton Content="Cursor" Margin="3" 
                          IsChecked="{Binding CursorEnabled, Mode=TwoWay}" />
        </StackPanel>

        <s:SciChartSurface x:Name="chart0" Grid.Row="1" s:ThemeManager.Theme="Chrome" Padding="30">
        <!-- ... -->

Next, lets ensure that the MainWindow.xaml has a viewmodel declared and sets the datacontext on the grid.

<!-- Note, your namespace for the viewmodel may be different.  -->
<Window x:Class="_08_Synchronize_Mouse_Events.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:s="clr-namespace:Abt.Controls.SciChart;assembly=Abt.Controls.SciChart.Wpf"
        xmlns:SynchronizeMouseEvents="clr-namespace:_08_Synchronize_Mouse_Events"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <SynchronizeMouseEvents:MultiChartMouseEventsViewModel x:Key="ViewModel"/>
    </Window.Resources>

    <!-- Ensure Grid.DataContext is set to the viewmodel instance -->
    <Grid DataContext="{StaticResource ViewModel}">

Note: There’s a myriad ways to create and bind to a viewmodel, this is the easiest for sake of demonstration, and also works in the Visual Studio designer! Don’t be confused by the syntax if you’re not familiar with binding. What this means is, create an instance of MultiChartMouseEventViewModel and set grid.DataContext equal to that instance.

If you run the application now, you should be able to see something like this. The toggle buttons should flip between Zoom/Pan and Rollover/Cursor and the default behaviour should be the cursor modifier. Don’t worry that the chart doesn’t zoom and pan just yet, we need to add some data.

How the charting app should look with toolbars

Adding Data to the Chart

We’re not far off having a working charting application, albeit with one chart. To finish the job, let’s add some data. We do this by binding the SciChartSurface to the ViewModel and creating DataSeriesSet properties in the viewmodel.

First, set up this binding in the view:

        <s:SciChartSurface x:Name="chart0" Grid.Row="1" s:ThemeManager.Theme="Chrome" Padding="30"
                           DataSet="{Binding ChartData0}">
            <!-- ... -->

And this code in the ViewModel:

    public class MultiChartMouseEventsViewModel : INotifyPropertyChanged
    {
        private bool mouseWheelEnabled;
        private bool panEnabled;
        private bool rolloverEnabled;
        private bool cursorEnabled;
        private bool zoomEnabled;
        private readonly IDataSeriesSet _chartData0;

        public MultiChartMouseEventsViewModel()
        {
            // Create a dataset which we will bind to
            _chartData0 = CreateDataset();

            // Set default states of modifiers
            this.MouseWheelEnabled = false;
            this.PanEnabled = true;
            this.CursorEnabled = true;

        }

        private IDataSeriesSet CreateDataset()
        {
            var ds = new DataSeriesSet<double, double>();
            var ds0 = ds.AddSeries();

            const int count = 1000;
            for (int i = 0; i < count; i++)
            {
                ds0.Append(i, count * Math.Sin(i * Math.PI * 0.1) / i);
            }

            return ds;
        }

        public IDataSeriesSet ChartData0 { get { return _chartData0; } }

// ... etc

Now if you run your application and double click on the chart to zoom to extents, you should see this:

A SIngle Chart with Toolbars and Data


Don’t see it? Here are some tips to debug why the chart is not showing:

  • Did you declare a single RenderableSeries on the SciChartSurface?
  • Did you add X and Y Axes to the chart? VisibleRange and GrowBy are not essential properties, but the chart won’t redraw without axes
  • Did you bind to the ViewModel and to the DataSet property ChartData0?
  • Did you fill the ChartData0 dataset with data?
  • Are there any binding errors in the output window of Visual Studio? If so, what?
  • Still stuck? Contact us and tell us to improve our quality of tutorials and debug error messages! We’d love to hear from you!

If you got this far you should now be able to perform the following actions on the chart.

  • Select Zoom to zoom into an area
  • Select pan to pan the chart
  • Use the mousewheel to zoom
  • Drag an X or Y Axis to zoom
  • Switch between Cursor and Rollover modifier
  • Double click to zoom to extents

Zoom Operations you should be able to perform

Synchronizing Multiple Charts

Now to the observant you will notice the title of the tutorial is “Synchronizing Mouse Events across charts”, so I guess we should add another chart!
To do this, simply copy & paste the entire tag, but renaming variables, e.g. chart0 to chart1, Grid.Row=”1” to Grid.Row=”2” and binding to ChartData0 to ChartData1. Also add another row in the parent Grid to host the chart. Your entire Xaml should now look like this:

<Window x:Class="_08_Synchronize_Mouse_Events.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:s="clr-namespace:Abt.Controls.SciChart;assembly=Abt.Controls.SciChart.Wpf"
        xmlns:SynchronizeMouseEvents="clr-namespace:_08_Synchronize_Mouse_Events"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <SynchronizeMouseEvents:MultiChartMouseEventsViewModel x:Key="ViewModel"/>
    </Window.Resources>

    <Grid DataContext="{StaticResource ViewModel}">
        <Grid.RowDefinitions>
            <RowDefinition Height="32"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>

        <!-- Define Toolbar -->
        <StackPanel Grid.Row="0" Orientation="Horizontal">
            <ToggleButton Content="Zoom" Margin="3" 
                          IsChecked="{Binding ZoomEnabled, Mode=TwoWay}"/>
            <ToggleButton Content="Pan" Margin="3" 
                          IsChecked="{Binding PanEnabled, Mode=TwoWay}"/>
            <ToggleButton Content="MouseWheel" Margin="3" 
                          IsChecked="{Binding MouseWheelEnabled, Mode=TwoWay}"/>
            <ToggleButton Content="Rollover" Margin="3" 
                          IsChecked="{Binding RolloverEnabled, Mode=TwoWay}"/>
            <ToggleButton Content="Cursor" Margin="3" 
                          IsChecked="{Binding CursorEnabled, Mode=TwoWay}"  />
        </StackPanel>

        <!-- Define the first chart -->
        <s:SciChartSurface x:Name="chart0" Grid.Row="1" s:ThemeManager.Theme="Chrome" Padding="30"
                           DataSet="{Binding ChartData0}">
            <s:SciChartSurface.RenderableSeries>
                <s:FastLineRenderableSeries SeriesColor="DarkBlue"></s:FastLineRenderableSeries>
            </s:SciChartSurface.RenderableSeries>

            <s:SciChartSurface.YAxis>
                <s:NumericAxis VisibleRange="0,1" GrowBy="0.1,0.1"></s:NumericAxis>
            </s:SciChartSurface.YAxis>

            <s:SciChartSurface.XAxis>
                <s:NumericAxis VisibleRange="0,1" GrowBy="0.1,0.1"></s:NumericAxis>
            </s:SciChartSurface.XAxis>

            <s:SciChartSurface.ChartModifier>
                <s:ModifierGroup>
                    <s:RubberBandXyZoomModifier IsEnabled="{Binding ZoomEnabled, Mode=TwoWay}" IsXAxisOnly="True"/>
                    <s:ZoomPanModifier IsEnabled="{Binding PanEnabled, Mode=TwoWay}"/>
                    <s:MouseWheelZoomModifier IsEnabled="{Binding MouseWheelEnabled, Mode=TwoWay}"/>
                    <s:RolloverModifier IsEnabled="{Binding RolloverEnabled, Mode=TwoWay}"/>
                    <s:CursorModifier IsEnabled="{Binding CursorEnabled, Mode=TwoWay}"/>
                    <s:YAxisDragModifier></s:YAxisDragModifier>
                    <s:XAxisDragModifier></s:XAxisDragModifier>
                    <s:ZoomExtentsModifier></s:ZoomExtentsModifier>
                </s:ModifierGroup>
            </s:SciChartSurface.ChartModifier>
        </s:SciChartSurface>

        <!-- Define the second chart -->
        <s:SciChartSurface x:Name="chart1" Grid.Row="2" s:ThemeManager.Theme="Chrome" Padding="30"
                           DataSet="{Binding ChartData1}">
            <s:SciChartSurface.RenderableSeries>
                <s:FastLineRenderableSeries SeriesColor="DarkBlue"></s:FastLineRenderableSeries>
            </s:SciChartSurface.RenderableSeries>

            <s:SciChartSurface.YAxis>
                <s:NumericAxis VisibleRange="0,1" GrowBy="0.1,0.1"></s:NumericAxis>
            </s:SciChartSurface.YAxis>

            <s:SciChartSurface.XAxis>
                <s:NumericAxis VisibleRange="0,1" GrowBy="0.1,0.1"></s:NumericAxis>
            </s:SciChartSurface.XAxis>

            <s:SciChartSurface.ChartModifier>
                <s:ModifierGroup>
                    <s:RubberBandXyZoomModifier IsEnabled="{Binding ZoomEnabled, Mode=TwoWay}" IsXAxisOnly="True"></s:RubberBandXyZoomModifier>
                    <s:ZoomPanModifier IsEnabled="{Binding PanEnabled, Mode=TwoWay}"></s:ZoomPanModifier>
                    <s:MouseWheelZoomModifier IsEnabled="{Binding MouseWheelEnabled, Mode=TwoWay}"></s:MouseWheelZoomModifier>
                    <s:RolloverModifier IsEnabled="{Binding RolloverEnabled, Mode=TwoWay}"></s:RolloverModifier>
                    <s:CursorModifier IsEnabled="{Binding CursorEnabled, Mode=TwoWay}"></s:CursorModifier>
                    <s:YAxisDragModifier></s:YAxisDragModifier>
                    <s:XAxisDragModifier></s:XAxisDragModifier>
                    <s:ZoomExtentsModifier></s:ZoomExtentsModifier>
                </s:ModifierGroup>
            </s:SciChartSurface.ChartModifier>
        </s:SciChartSurface>
    </Grid>
</Window>

In Code-behind, add a Loaded event handler to zoom the two charts to extents on startup:

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            chart0.AnimateZoomExtents(TimeSpan.FromMilliseconds(1500));
            chart1.AnimateZoomExtents(TimeSpan.FromMilliseconds(1500));
        }
    }

And in the ViewModel ensure you have two DataSeriesSet properties for the two charts:

        public MultiChartMouseEventsViewModel()
        {
            // Create two datasets which we will bind to
            _chartData0 = CreateDataset();
            _chartData1 = CreateDataset();

            // Set default states of modifiers
            this.MouseWheelEnabled = false;
            this.PanEnabled = true;
            this.CursorEnabled = true;

            // ...
        }

        public IDataSeriesSet ChartData0 { get { return _chartData0; } }
        public IDataSeriesSet ChartData1 { get { return _chartData1; } }
        // ...

In the designer window in Visual Studio you should see something like this

Dual Chart Designer Window View

And when you run the application, something like this:

How the app should look when you run – with dual charts

Synchronizing the two charts

Although the application looks good, you will notice the two charts don’t have their mouse events synchronized. To do this we need to make a few changes to the Xaml and the ViewModel.

Bind both XAxis VisibleRanges to a shared property in the viewmodel. This serves the purpose that any change on one chart moves the XAxis of the other chart to match it.

View (Note, ensure this binding is on both charts XAxis)

        <!-- Define the Nth chart --> 
        <s:SciChartSurface x:Name="chart1" Grid.Row="2" s:ThemeManager.Theme="Chrome" Padding="30"
                           DataSet="{Binding ChartData1}">
        <!-- ... -->
            <s:SciChartSurface.XAxis>
                <s:NumericAxis VisibleRange="{Binding SharedXVisibleRange, Mode=TwoWay}" GrowBy="0.1,0.1"/>
            </s:SciChartSurface.XAxis>
        <!-- ... -->

ViewModel

    public class MultiChartMouseEventsViewModel : INotifyPropertyChanged
    {
        private bool mouseWheelEnabled;
        private bool panEnabled;
        private bool rolloverEnabled;
        private bool cursorEnabled;
        private bool zoomEnabled;
        private IRange sharedXVisibleRange;
        private readonly IDataSeriesSet _chartData0;
        private readonly IDataSeriesSet _chartData1;

        public MultiChartMouseEventsViewModel()
        {
            // Create two datasets which we will bind to
            _chartData0 = CreateDataset();
            _chartData1 = CreateDataset();

            // Set default states of modifiers
            this.MouseWheelEnabled = false;
            this.PanEnabled = true;
            this.CursorEnabled = true;

            // Set default shared XAxis VisibleRange which multiple charts will bind to
            SharedXVisibleRange = new DoubleRange(0, 1);
        }

        public IRange SharedXVisibleRange
        {
            get { return sharedXVisibleRange; }
            set
            {
                if (sharedXVisibleRange == value) return;
                sharedXVisibleRange = value;
                OnPropertyChanged("SharedXVisibleRange");
            }
        }
        // ...

Next, Use the MouseManager.MouseEventGroup on the ModifierGroups of both charts. This serves the purpose to pass mouse events between one chart and another, regardless of which chart started the mouse-event.

<s:SciChartSurface.ChartModifier>
    <s:ModifierGroup mouse:MouseManager.MouseEventGroup="MyCustomGroup">

Now running the application you should see that mouse events and chart modifiers are shared across the chart!

Mouse events synchronized across charts! But …

But wait, there are a few quirks. Notice if you select Pan mode, and drag to pan, the rollover or cursor on one chart freezes?

Some quirks in the synchronized mouse events, which we are going to sort out

Also notice if you zoom using the Rubber Band then only one chart has the zoom rectangle on it?

Let’s continue by fixing these quirks before we call the example done.

Fixing the Quirks, MouseManager Handled Events

The MouseManager works in a similar way to the WPF tunnelling events. If you have N ChartModifiers in a ModifierGroup, and one modifier sets e.Handled=true, then subsequent modifiers will not receive the event.

To override this behaviour, we can set the ReceiveHandledEvents property to true on chart modifiers:

            <s:SciChartSurface.ChartModifier>
                <s:ModifierGroup mouse:MouseManager.MouseEventGroup="MyCustomGroup">
                    <s:RubberBandXyZoomModifier ReceiveHandledEvents="True" IsEnabled="{Binding ZoomEnabled, Mode=TwoWay}" IsXAxisOnly="True"></s:RubberBandXyZoomModifier>
                    <s:ZoomPanModifier IsEnabled="{Binding PanEnabled, Mode=TwoWay}"></s:ZoomPanModifier>
                    <s:MouseWheelZoomModifier IsEnabled="{Binding MouseWheelEnabled, Mode=TwoWay}"></s:MouseWheelZoomModifier>
                    <s:RolloverModifier ReceiveHandledEvents="True" IsEnabled="{Binding RolloverEnabled, Mode=TwoWay}"></s:RolloverModifier>
                    <s:CursorModifier ReceiveHandledEvents="True" IsEnabled="{Binding CursorEnabled, Mode=TwoWay}"></s:CursorModifier>
                    <s:YAxisDragModifier></s:YAxisDragModifier>
                    <s:XAxisDragModifier></s:XAxisDragModifier>
                    <s:ZoomExtentsModifier></s:ZoomExtentsModifier>
                </s:ModifierGroup>
            </s:SciChartSurface.ChartModifier>

Note: set this property on the modifiers on both Chart Surfaces

Next run the app – you should see it behaves as expected, et voila! Mouse events are shared across charts!

The completed tutorial application. Quirks be gone!

Executable and downloadable

This tutorial in Word Doc and Source code form can be downloaded along with the SciChart Trial at www.scichart.com/downloads/

Note this requires free registration with the site to get the tutorial.

For your convenience, we’ve also included the SynchronizeMouseEvents tutorial source code here. however note it doens’t have the SciChart binaries, you’ll need to get those off the downloads page.

Thanks and enjoy!


Read more »




CONTACT US

Not sure where to start? Contact us, we are happy to help!


CONTACT US

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL. Email: Legal Company Number: 07430048, VAT Number: 101957725