A few weeks ago I published a blog post which compared the performance of the Visiblox charts and the Silverlight Toolkit charts. The results indicated that the Visblox charts are considerably faster than the Toolkit charts, however Microsoft's David Anson did point out that the Toolkit charts were not designed with performance in mind, and that a comparison would be more fair if the 'FastLineSeries' series that has been on the TODO list for a while were implemented.

UPDATE: I have published a more up-to-date and extensive test of WPF charting components in a more recent blog post. This new test looks at how raster-graphics can be used to significantly enhance performance.

I have been asked by a few people to extend the performance test to include a few other Silverlight charts to see how they compare. I have refactored the test code so that different charts can be plugged in more easily. This not only makes it easier to test the performance of other charting components, but has also allowed me to compare the different charting APIs more easily.

This blog post compares the performance of Visiblox, Visifire, Silverlight Toolkit and Dynamic Data Display (D3) charts. I also compared a few other charting components, however the terms of their trial license prohibit me from publishing the results.

A summary of the performance measurement is given below, where each chart is used to plot rapidly changing data, with the framerate being used as a measure of performance.

The test clients running with each of the different charts are shown below:

Visiblox Chart Visifire Chart
Silverlight Toolkit Chart D3 Chart

[cheetah image courtesy of flickrfavorites, snail image courtesy of Per Ola Wiberg, tortoise image courtesy of wwarby, horse image courtesy of Linnéa Gröndalen]

We'll look at the XAML markup for each chart and the code-behind used to add data to each chart in the following sections:

Visiblox

The XAML markup for the Visiblox charts is reasonably concise, with the X & Y axes configured and styled and the three LineSeries added to the chart:

<UserControl.Resources>
  <Style TargetType="Line" x:Key="GridLineStyle">
    <Setter Property="Stroke" Value="#dddddd" />
    <Setter Property="StrokeThickness" Value="1" />
  </Style>
</UserControl.Resources>

<Grid x:Name="LayoutRoot" Background="White">
  <chart:Chart x:Name="chart"
                  LegendVisibility="Collapsed"
                  Margin="5,0,5,5">
    <chart:Chart.XAxis>
      <!-- a 'hidden' X axis -->
      <chart:LinearAxis ShowAxis="False"
                            GridlineStyle="{StaticResource GridLineStyle}"/>
    </chart:Chart.XAxis>
    <chart:Chart.YAxis>
      <!-- A Y axis with labels within the chart area -->
      <chart:LinearAxis ShowLabels="True"
                            LabelsPosition="Inside"
                            GridlineStyle="{StaticResource GridLineStyle}"
                            ShowMajorTicks="False"
                            ShowMinorTicks="False"/>
    </chart:Chart.YAxis>
    <chart:Chart.Series>
      <!-- the series used to render the RGB components -->
      <chart:LineSeries LineStroke="#A00"/>
      <chart:LineSeries LineStroke="#0A0"/>
      <chart:LineSeries LineStroke="#00A"/>
    </chart:Chart.Series>
  </chart:Chart>
</Grid>

The code-behind is also reasonably concise with the list of DataPoint objects returned by the test-harness converted into Visiblox DataSeries via a Linq Select projection.

protected override void RenderDataToChart(List<List<Histogram.DataPoint>> rgbData)
{
  _chart.Chart.Series[0].DataSeries = ListToSeries(rgbData[0]);
  _chart.Chart.Series[1].DataSeries = ListToSeries(rgbData[1]);
  _chart.Chart.Series[2].DataSeries = ListToSeries(rgbData[2]);
}

private DataSeries<double, double> ListToSeries(List<Histogram.DataPoint> data)
{
  return new DataSeries<double, double>(data.Select(pt =>
    new DataPoint<double, double>(pt.Location, pt.Intensity)));
}

Visifire

Visifire also has a reasonably concise XAML markup, however in order to provide maximum performance, there are a number of chart features that need to be disabled. The Visifire charts have a Line series which could be used for this comparison, however they recently released a QuickLine series type which removes certain features to provide better performance.

<Grid x:Name="LayoutRoot" Background="White">
  <vc:Chart x:Name="chart"
                AnimationEnabled="False"
                ScrollingEnabled="False">

    <vc:Chart.Legends>
      <vc:Legend Enabled="False"/>
    </vc:Chart.Legends>

    <!-- the series used to render the RGB components -->
    <vc:Chart.Series>
      <vc:DataSeries RenderAs="QuickLine"
                      Color="#A00" LineThickness="1"
                      MarkerEnabled="False" ShadowEnabled="False" />
      <vc:DataSeries RenderAs="QuickLine"
                      Color="#0A0" LineThickness="1"
                      MarkerEnabled="False" ShadowEnabled="False" />
      <vc:DataSeries RenderAs="QuickLine"
                      Color="#00A" LineThickness="1"
                      MarkerEnabled="False" ShadowEnabled="False" />
    </vc:Chart.Series>

    <!-- a 'hidden' X axis -->
    <vc:Chart.AxesX>
      <vc:Axis Enabled="False">
        <vc:Axis.Grids>
          <vc:ChartGrid Enabled="True"
                        LineColor="#ddd" LineThickness="1"/>
        </vc:Axis.Grids>
      </vc:Axis>
    </vc:Chart.AxesX>

    <vc:Chart.PlotArea>
      <vc:PlotArea ShadowEnabled="False"/>
    </vc:Chart.PlotArea>
  </vc:Chart>
</Grid>

The code-behind required to add data to the chart is straightforward. However, you cannot simply create a new DataPointCollection and replace the current Series.DataPoints, instead you have to clear the existing collection, then rebuild it.

protected override void RenderDataToChart(List<List<Histogram.DataPoint>> rgbData)
{
  AddDataToSeries(_chart.Chart.Series[0], rgbData[0]);
  AddDataToSeries(_chart.Chart.Series[1], rgbData[1]);
  AddDataToSeries(_chart.Chart.Series[2], rgbData[2]);
}

private void AddDataToSeries(DataSeries series, List<Histogram.DataPoint> list)
{
  series.DataPoints.Clear();
  foreach (var pt in list)
  {
    series.DataPoints.Add(new Visifire.Charts.DataPoint()
    {
      XValue = pt.Location,
      YValue = pt.Intensity
    });
  }
}

Silverlight Toolkit

The Silverlight Toolkit XAML is quite verbose, mostly due to the performance enhancements described in my previous blog post.

<UserControl.Resources>
  <Style TargetType="dataVis:Legend" x:Key="CollapsedLegendStyle">
    <Setter Property="Visibility" Value="Collapsed"/>
    <Setter Property="Width" Value="0"/>
  </Style>

  <Style TargetType="Control" x:Key="CollapsedStyle">
    <Setter Property="Visibility" Value="Collapsed"/>
  </Style>

  <ControlTemplate x:Key="SimplifiedDataPoint" TargetType="tk:LineDataPoint">
  </ControlTemplate>
</UserControl.Resources>

<Grid x:Name="LayoutRoot" Background="White">
  <tk:Chart x:Name="chart"
        LegendStyle="{StaticResource CollapsedLegendStyle}"
        TitleStyle="{StaticResource CollapsedStyle}">

    <!-- define the line series -->
    <tk:LineSeries ItemsSource="{Binding}"
                TransitionDuration="0"
                DependentValueBinding="{Binding Intensity}"
                IndependentValueBinding="{Binding Location}">
      <tk:LineSeries.DataPointStyle>
        <Style TargetType="tk:LineDataPoint">
          <Setter Property="Visibility" Value="Collapsed"/>
          <Setter Property="Template" Value="{StaticResource SimplifiedDataPoint}"/>
          <Setter Property="Background" Value="#A00"/>
        </Style>
      </tk:LineSeries.DataPointStyle>
    </tk:LineSeries>

    <tk:LineSeries ItemsSource="{Binding}"
                TransitionDuration="0"
                DependentValueBinding="{Binding Intensity}"
                IndependentValueBinding="{Binding Location}">
      <tk:LineSeries.DataPointStyle>
        <Style TargetType="tk:LineDataPoint">
          <Setter Property="Visibility" Value="Collapsed"/>
          <Setter Property="Template" Value="{StaticResource SimplifiedDataPoint}"/>
          <Setter Property="Background" Value="#0A0"/>
        </Style>
      </tk:LineSeries.DataPointStyle>
    </tk:LineSeries>

    <tk:LineSeries ItemsSource="{Binding}"
                TransitionDuration="0"
                DependentValueBinding="{Binding Intensity}"
                IndependentValueBinding="{Binding Location}">
      <tk:LineSeries.DataPointStyle>
        <Style TargetType="tk:LineDataPoint">
          <Setter Property="Visibility" Value="Collapsed"/>
          <Setter Property="Template" Value="{StaticResource SimplifiedDataPoint}"/>
          <Setter Property="Background" Value="#00A"/>
        </Style>
      </tk:LineSeries.DataPointStyle>
    </tk:LineSeries>

    <!-- configure the axes -->
    <tk:Chart.Axes>
      <tk:LinearAxis Orientation="X" Height="0">
      </tk:LinearAxis>
    </tk:Chart.Axes>
  </tk:Chart>
</Grid>

However, the code-behind is the most concise of all the charts I tested because the chart databinds directly to the properties of the DataPoint business objects.

protected override void RenderDataToChart(List<List<DataPoint>> rgbData)
{
  ((LineSeries)_chart.Chart.Series[0]).ItemsSource = rgbData[0];
  ((LineSeries)_chart.Chart.Series[1]).ItemsSource = rgbData[1];
  ((LineSeries)_chart.Chart.Series[2]).ItemsSource = rgbData[2];
}

Dynamic Data Display

The D3 charts are quite different to the others which I have tested. These charts were developed by a Microsoft team based in Russia and are released on codeplex. They claim outstanding performance with large volumes of data, and are often recommended on sites such as stackoverflow when questions relating to charting performance arise.

The XAML markup for the D3 charts is pretty minimal!

<Grid x:Name="LayoutRoot" Background="White">
  <d3:ChartPlotter Name="plotter">
  </d3:ChartPlotter>
</Grid>

However, this is because the D3 charts do not support configuration via markup. It took me quite a while to work out how to configure the charts in code-behind, the D3 APIs are not very intuitive. I am not the only one to have noticed this; Lee Campbell, in his "WPF Charting Comparisons" blog post stated " ... it took me hours of reading forums, looking at samples and coding to just get my Model showing on the screen". Oh dear!

I had to resort to navigating the visual tree (using Linq to VisualTree) to locate the pan and zoom controls which are added to the chart by default, and remove them.

However, in fairness, the Silverlight version of D3 charts is a partial port of the WPF version, but both share a similar programmatic style of usage.

The following code is used to configure the chart in code-behind:

public DDDChart()
{
  InitializeComponent();

  this.Loaded += new RoutedEventHandler(DDDChart_Loaded);
}

private LineGraph InitGraph(Color color)
{
  var animatedX = new List<double>();
  var animatedY = new List<double>();

  // add some points, otherwise we get some layout-related exception
  animatedX.Add(0);
  animatedY.Add(0);

  // create the X & Y sources
  _xSource = new EnumerableDataSource<double>(animatedX);
  _xSource.SetXMapping(x => x);
  _ySource = new EnumerableDataSource<double>(animatedY);
  _ySource.SetYMapping(y => y);

  // Adding graph to plotter
  var graph = new LineGraph(new CompositeDataSource(_xSource, _ySource), "");
  graph.LineColor = color;
  graph.LineThickness = 1;
  plotter.Children.Add(graph);

  return graph;
}

private void DDDChart_Loaded(object sender, RoutedEventArgs e)
{
  // remove the mouse pan and zoom controls
  var navigationControl = plotter.Children.OfType<MouseNavigation>().Single();
  plotter.Children.Remove(navigationControl);

  var zoomControl = plotter.Descendants().OfType<buttonsNavigation>().Single();
  ((Panel)zoomControl.Ancestors().First()).Children.Remove(zoomControl);

  _graphOne = InitGraph(Color.FromArgb(255,200,0,0));
  _graphTwo = InitGraph(Color.FromArgb(255, 0, 200, 0));
  _graphThree = InitGraph(Color.FromArgb(255, 0, 0, 200));

  // Force everything plotted to be visible
  plotter.FitToView();

  plotter.Legend.Visibility = Visibility.Collapsed;

}

The following code is used to change the data for each series. Again, the D3 APIs are a little complex. Also, the chart does not have a mode where the X & Y axes compute their range based on the data. Instead, this is performed programatically via FitToView.

protected override void RenderDataToChart(List<List<DataPoint>> rgbData)
{
  RenderDataToGraph(_chart.LineGraphR, rgbData[0]);
  RenderDataToGraph(_chart.LineGraphG, rgbData[1]);
  RenderDataToGraph(_chart.LineGraphB, rgbData[2]);

  // re-scale the chart based on the rendered data
  _chart.Plotter.FitToView();
}

private void RenderDataToGraph(LineGraph graph, List<DataPoint> histogram)
{
  var source = graph.DataSource as CompositeDataSource;

  // obtain the two sources which the composite is composed of
  var xSource = source.DataParts.ElementAt(0) as EnumerableDataSource<double>;
  var ySource = source.DataParts.ElementAt(1) as EnumerableDataSource<double>;

  var dataX = new List<double>();
  var dataY = new List<double>();
  foreach(var point in histogram)
  {
    dataX.Add(point.Location);
    dataY.Add(point.Intensity);
  }
  xSource.Data = dataX;
  ySource.Data = dataY;
}

Conclusions

In summary, the Visiblox and Visifire charts seem to have the cleanest API, with concise XAML markup to define the charts and little code-behind required to update the series. The Silverlight Toolkit charts XAML markup is a bit verbose, mostly due to performance optimisation. Finally, the D3 charts have a complex and difficult to follow API. In terms of performance, the Visiblox and D3 charts give similar results, the Visifire charts are a little behind with, with a frame rate that is approximately 1/3 of the leading charts, and the Silverlight Toolkit charts come last.

You can download the full sourcecode of this comparison here: ChartPerformance.zip

NOTE: To build the examples you need to download the charting components from their respective web pages:

Finally, a couple of my colleagues have created similar charting tests using other technologies, you might be interested in Graham's Flex implementation, or Chris's cute HTML5 implementation.

Regards, Colin E.