Colin Eberhardt's Adventures in .NET

Adding Error Bars to Visiblox Silverlight Charts

October 21st, 2011

Having spent a number of years studying Physics at university, I have had the importance of error bars well and truly drummed into me! Within physics, or any experimental science, there are always going to be errors in the measurements you make. The more repeat measurements you make, the more confident you can be in the mean value, however you cannot remove the errors altogether. Error bars provide a way to represent the spread of experimental observations graphically, without them, it is hard to have any confidence in the conclusions drawn from the observations!

In this blog post I will show how to implement a custom Visiblox chart series to render error bars:

(The data in the above chart is from a page which details how to calculate the standard error from experimental results).

Creating a Custom Series

As described in my previous blog post on creating a spline series, to create a new series type, you sub-class one of the Visiblox base-classes, in this case MultiValueSeriesBase is a suitable starting point:

public class ErrorBarSeries : MultiValueSeriesBase
{ 
  protected override FrameworkElement CreatePoint(IDataPoint dataPoint)
  {
    throw new NotImplementedException();
  }
 
  protected override void RenderDataLabels()
  {
    throw new NotImplementedException();
  }
}

I don’t want data labels, so the only method I need to implement is CreatePoint, which takes the (multi-valued) point to be rendered as its only argument. The lifecycle of point creating and destruction is taken care of by the base-class.

The IDataPoint has a string indexer which is used to retrieve multiple Y values for multi-valued series. It is a good idea to define these in a single place, here we define the three y-values required for an error-bar series:

public static readonly string ErrorUp = "ErrorUp";
 
public static readonly string ErrorDown = "ErrorDown";
 
public static readonly string Value = "Value";

The CreatePoint implementation for this series creates a Path as follows:

protected override FrameworkElement CreatePoint(IDataPoint dataPoint)
{
  var lineGeometry = BuildGeometry(dataPoint);
 
  Path line = new Path();
  line.Stroke = new SolidColorBrush(Colors.Black);
  line.Fill = new SolidColorBrush(Colors.Gray);
  line.StrokeThickness = 1.0;
  line.StrokeLineJoin = PenLineJoin.Bevel;
  line.Data = lineGeometry;
  line.SetValue(ZoomCanvas.IsScaledPathProperty, true);
 
  return line;
}

The BuildGeometry method does most of the work, extracting the values from the IDataPoint, transforming them (via the axis) to the required coordinate system, then creating a suitable geometry:

/// <summary>
/// Creates the geometry for the given datapoint
/// </summary>
private PathGeometry BuildGeometry(IDataPoint dataPoint)
{
  var halfWidth = SuggestedPointWidth * WidthFactor;
 
  // obtain the data values
  var topDataValue = dataPoint[ErrorUp] as IComparable;
  var middleDataValue = dataPoint[Value] as IComparable;
  var bottomDataValue = dataPoint[ErrorDown] as IComparable;
 
  // convert to a the required render coordinates
  double topRenderPos = YAxis.GetDataValueAsRenderPositionWithoutZoom(topDataValue);
  double middleRenderPos = YAxis.GetDataValueAsRenderPositionWithoutZoom(middleDataValue);
  double bottomRenderPos = YAxis.GetDataValueAsRenderPositionWithoutZoom(bottomDataValue);
 
  double xMiddleRenderPos = XAxis.GetDataValueAsRenderPositionWithoutZoom(dataPoint.X);
  double xRightRenderPos = xMiddleRenderPos - halfWidth;
  double xLeftRenderPos = xMiddleRenderPos + halfWidth;
 
  // build a suitable gemoetry
  PathGeometry lineGeometry = new PathGeometry();
 
  PathFigure upperVerticalLine = CreateLineFigure(
    new Point(xMiddleRenderPos, middleRenderPos - halfWidth),
    new Point(xMiddleRenderPos, topRenderPos));
  lineGeometry.Figures.Add(upperVerticalLine);
 
  PathFigure lowerVerticalLine = CreateLineFigure(
    new Point(xMiddleRenderPos, bottomRenderPos),
    new Point(xMiddleRenderPos, middleRenderPos + halfWidth));
  lineGeometry.Figures.Add(lowerVerticalLine);
 
  PathFigure upperBar = CreateLineFigure(
    new Point(xLeftRenderPos, topRenderPos),
    new Point(xRightRenderPos, topRenderPos));
  lineGeometry.Figures.Add(upperBar);
 
  PathFigure lowerBar = CreateLineFigure(
    new Point(xLeftRenderPos, bottomRenderPos),
    new Point(xRightRenderPos, bottomRenderPos));
  lineGeometry.Figures.Add(lowerBar);
 
  PathFigure center = CreateLineFigure(
    new Point(xMiddleRenderPos - halfWidth, middleRenderPos),
    new Point(xMiddleRenderPos, middleRenderPos + halfWidth),
    new Point(xMiddleRenderPos + halfWidth, middleRenderPos),
    new Point(xMiddleRenderPos, middleRenderPos - halfWidth)
  );
  lineGeometry.Figures.Add(center);
 
  return lineGeometry;
}
 
/// <summary>
/// Create a line figure that connects the given points
/// </summary>
private PathFigure CreateLineFigure(params Point[] points)
{
  // add all the points (except the first)
  var pointCollection = new PointCollection();
  foreach (var point in points.Skip(1))
  {
    pointCollection.Add(point);
  }
 
  // create a figure, using the first point as the StartPoint.
  return new PathFigure()
  {
    IsClosed = true,
    StartPoint = points.First(),
    Segments = new PathSegmentCollection()
    {
      new PolyLineSegment
      {
        Points = pointCollection
      }
    }
  };
}

We can now create an instance of this series in XAML:

<vis:Chart x:Name="chart">
  <vis:Chart.Series>
    <local:ErrorBarSeries/>
  </vis:Chart.Series>
</vis:Chart>

Supplying data to the chart via MultiValuedDataPoint as follows:

public MainPage()
{
  InitializeComponent();
 
  var data = new DataSeries<double, double>();
  data.Add(CreatePoint(-195, 1.4, 0.2));
  data.Add(CreatePoint(0, 62.2, 9.3));
  data.Add(CreatePoint(20, 70.4, 6.5));
  data.Add(CreatePoint(100, 77.4, 1.9));
 
  chart.Series[0].DataSeries = data;
}
 
private MultiValuedDataPoint<double, double> CreatePoint(double x, double y, double error)
{
  var point = new MultiValuedDataPoint<double, double>(x,
    new Dictionary<object, double>()
    {
      { ErrorBarSeries.ErrorUp, y + error },
      { ErrorBarSeries.ErrorDown, y - error },
      { ErrorBarSeries.Value, y }
    });
  return point;
}

This results in the following chart:

Binding to a Multi-valued Series

In the previous example we created instances of MultiValuedDataPoint, a Visiblox type for representing multi-valued points. As an alternative, we can create model objects to represent each point, rendering them in the chart via databinding.

We first modify the code to create a collection of Measurement instances (a simple model object that implements INotifyPropertyChanged):

public MainPage()
{
  InitializeComponent();
 
  var data = new ObservableCollection<Measurement>();
  data.Add(CreateMeasurement(-195, 1.4, 0.2));
  data.Add(CreateMeasurement(0, 62.2, 9.3));
  data.Add(CreateMeasurement(20, 70.4, 6.5));
  data.Add(CreateMeasurement(100, 77.4, 1.9));
  this.DataContext = data;    
}
 
private Measurement CreateMeasurement(double x, double y, double error)
{
  return new Measurement()
    {
      XValue = x,
      YValue = y,
      YValueErrorUp = y + error,
      YValueErrorDown = y - error
    };
}

The markup for the chart is modified to use a BindableDataSeries, with bindings specified for the various component of the error bar series. Also, the ItemsSource of the BindableDataSeries is bound to the inherited DataContext:

<local:ErrorBarSeries>
  <local:ErrorBarSeries.DataSeries>
    <vis:BindableDataSeries ItemsSource="{Binding}"
                            XValueBinding="{Binding XValue}">
      <vis:BindableDataSeries.YValueBindings>
        <vis:YValueBinding YValueKey="Value" Binding="{Binding YValue}"/>
        <vis:YValueBinding YValueKey="ErrorUp" Binding="{Binding YValueErrorUp}"/>
        <vis:YValueBinding YValueKey="ErrorDown"  Binding="{Binding YValueErrorDown}"/>
      </vis:BindableDataSeries.YValueBindings>              
    </vis:BindableDataSeries>
  </local:ErrorBarSeries.DataSeries>
</local:ErrorBarSeries>

We can also display our data in a DataGrid, allowing us to manipulate the values (not that I condone manipulation of scientific data!), with the changes being reflected in the chart:

<sdk:DataGrid Grid.Row="1"
              x:Name="grid"
              ItemsSource="{Binding}"/>

This gives us the following application:

You can edit the values, with the changes reflected immediately in the chart above.

You can download the sourcecode for the above example here: VisibloxErrorBarSeries.zip

Regards, Colin E.

#uksnow #silverlight The Movie! – Happy Christmas.

December 24th, 2010

It’s Christmas Eve and time for some fun! A few weeks back I published an article on Reactive Extensions where I created a Bing Maps / Twitter mashup that plotted the geolocation of #uksnow twitter tags. This twitter hashtag was popularised by Ben Marsh and is used by us weather obsessed brits to give a realtime update of snowfall. To use this hashtag tweet #uksnow with your postcode and the amount of snowfall out of ten (e.g. 7/10).

The snow has continued to fall in record quantities across the uk, so I thought it might be fun to create “#uksnow The Movie”, where you can replay the snowfall across the UK. You can see the finished app below. Hit the ‘play’ button, or drag the time indicator on the chart to replay the snowfall and re-live the fun! You can also mouse-over the snowflakes to see the original #uksnow tweet.

To create this application I modified my original app to extract as many #uksnow tweets from twitter as possible. Unfortunately the public search API only server tweets from the last ~ 7 days. If anyone knows of a free historic service, please let me know!

The application above contains an XML file with all the historic tweets. When it starts up, it parses the XML file.The chart displays the tweets ‘binned’ in 30 minute intervals, which is achieved via a simple GroupBy query:

// parse the XML file
_tweets = doc.Descendants("tweet").Select(el => new GeoCodedUKSnowTweet()
                                  {
                                    Author = el.Attribute("author").Value,
                                    Title = el.Attribute("title").Value,
                                    ProfileImageUrl = el.Attribute("profile").Value,
                                    Location = StringToPoint(el.Attribute("loc").Value),
                                    SnowFactor = int.Parse(el.Attribute("snowfactor").Value),
                                    Timestamp = DateTime.Parse(el.Attribute("timestamp").Value)
                                  })
                                  .OrderBy(t => t.Timestamp)                                        
                                  .ToList();
 
// total the tweets per 30 minute interval
var groupedByHour = _tweets.GroupBy(t => RoundDateToMinutes(t.Timestamp, 30))
                          .Select(group => 
                            new TweetsPerHour() {
                              Hour = group.Key,
                              Tweets = group.Count()
                            }).ToList();
 
// associate the above with the chart
chart.Series[0].YAxis = chart.SecondaryYAxis;
((BindableDataSeries)chart.Series[0].DataSeries).ItemsSource = groupedByHour;

I used the free Visiblox chart to display the tweet frequency. A LineSeries with a BindableDataSeries is used to render the results of the query above using databinding.

<vis:LineSeries.DataSeries>
  <vis:BindableDataSeries
         XValueBinding="{Binding Path=Hour}"
         YValueBinding="{Binding Path=Tweets}"/>
</vis:LineSeries.DataSeries>

The Visiblox chart has a pluggable behaviour concept which allows you to add interactivity to the chart. The vertical line that indicates the current time uses this behaviour framework. When the user interacts with the chart via mouse movements, these interactions are directed to any active behaviours, which can then update their state, possibly interacting with the chart or its axes. For example, when the code which updates the date as the user drags the line is shown below:

public override void MouseLeftButtonDown(Point position)
{
  base.MouseLeftButtonDown(position);
  _mouseDown = true;
}
 
public override void MouseLeftButtonUp(Point position)
{
  base.MouseLeftButtonUp(position);
  _mouseDown = false;
}
 
public override void MouseMove(Point position)
{
  if (_mouseDown)
  {
    CurrentDate = (DateTime)Chart.XAxis.GetRenderPositionAsDataValueWithZoom(position.X);
  }
}

For the sake of simplicity, the calendar and clock controls are implemented as UserControls, for example the calendar has the following markup:

<Grid x:Name="LayoutRoot" Background="Transparent">
  <Border CornerRadius="8"
            Background="White"
            Margin="5">
    <Border.Effect>
      <DropShadowEffect Color="LightGray"
                        Opacity="0.8"
                        ShadowDepth="2"/>
    </Border.Effect>
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="15"/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <Border CornerRadius="8,8,0,0"
              BorderThickness="0">
        <Border.Background>
          <LinearGradientBrush StartPoint="0,0"
                                EndPoint="0,1">
            <GradientStop Color="White" Offset="0"/>
            <GradientStop Color="Red" Offset="0.9"/>
            <GradientStop Color="DarkRed" Offset="1"/>
          </LinearGradientBrush>
        </Border.Background>
      </Border>
      <TextBlock Text="Friday"
                  x:Name="DayOfWeek"
                  FontWeight="Bold"
                  FontSize="8"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center"/>
      <TextBlock Text="28"
                  x:Name="Day"
                  Grid.Row="1"
                  FontWeight="Bold"
                  FontSize="20"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center"/>
    </Grid>    
  </Border>
</Grid>

And the following minimal code-behind:

public partial class CalendarControl : UserControl
{
  public CalendarControl()
  {
    InitializeComponent();
  }
 
  public void SetDate(DateTime date)
  {
    DayOfWeek.Text = date.ToString("ddd");
    Day.Text = date.ToString("dd");
  }
}

The clock control is very similar, using a few concepts that I wrote abut in my gauge control blog post a few weeks back. It is good to see that Silverlight allows quick and direct implementations of controls, you do not always have to use MVVM, lookless controls and dependency properties. If it is Christmas Eve, and you are itching to get home and grab a drink, anything goes ;-)

You can download the full sourcecode here: UKSnowTheMovie.zip

Right … I’m off home.

Regards, Colin E.

Visiblox, Visifire, DynamicDataDisplay – Charting Performance Comparison

December 10th, 2010

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.

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.

Adding a Smoothed Line Series (Bézier curve) to a Visiblox Chart

November 22nd, 2010

In this blog post I look at how to add a new series type to the Visiblox charts by creating my own series type which renders a smoothed line using a Bézier curve.

This blog post describes how to create a new series type for the Visiblox charts, a spline series. The example below shows the new series type in action, with the various spline control points rendered (just for fun!):


Creating a New Series Type

The visual representation of a Visiblox DataSeries is the responsibility of a chart series, as defined by the IChartSeries interface. However, the abstract baseclass ChartSeriesBase is most often the best place to start when creating a new series type.

The first step towards creating my smoothed series type is to create a ChartSeries subclass. The only abstract method that we must implement is InvalidateInternal, and it is here that we will place our logic to create the series:

public class SplineSeries : ChartSeriesBase
{
 
  public SplineSeries()
  {
    DefaultStyleKey = typeof(SplineSeries);
  }
 
  protected override void InvalidateInternal()
  {
    // render logic goes here
  }
}

A template also has to be supplied for the series in the generic.xaml file. The ChartSeriesBase requires that a ZoomCanvas element is located within the template as follows:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:VisibloxSplineSeries"
    xmlns:visPrim="clr-namespace:Visiblox.Charts.Primitives;assembly=Visiblox.Charts">
 
  <Style TargetType="local:SplineSeries">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="local:SplineSeries">
          <visPrim:ZoomCanvas x:Name="LayoutRoot" />
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
 
</ResourceDictionary>

That’s all the boiler plate code out of the way with, let’s get down to writing our series implementation.

A Simple Line Series

We’ll start by creating a straight-line implementation. Within the UpdateInternal method the following code constructs a Path instance and adds it to the ZoomCanvas:

protected override void InvalidateInternal()
{
  RootZoomCanvas.Children.Clear();
 
  Path path = new Path();
  PathGeometry geometry = new PathGeometry();
  PathFigure figure = new PathFigure();
 
  var renderPoints = GetRenderPoints();
  figure.StartPoint = renderPoints.First();
  foreach (var renderPoint in renderPoints.Skip(1))
  {
    figure.Segments.Add(new LineSegment()
    {
      Point = renderPoint
    });
  }
 
  geometry.Figures.Add(figure);
  path.Data = geometry;
  path.StrokeThickness = 2;
  path.Stroke = new SolidColorBrush(Colors.Black);
 
  ZoomCanvas.SetIsScaledPath(path, true);
 
  RootZoomCanvas.Children.Add(path);
}
 
public List<Point> GetRenderPoints()
{
  var renderPoints = new List<Point>();
  foreach (IDataPoint point in this.DataSeries)
  {
    double xPos = XAxis.GetDataValueAsRenderPositionWithoutZoom(point.X);
    double yPos = YAxis.GetDataValueAsRenderPositionWithoutZoom(point.Y);
    renderPoints.Add(new Point(xPos, yPos));
  }
  return renderPoints;
}

Let’s look at this code in detail. The method GetRenderPoints creates a list of points which indicate the location where each datapoint should be rendered on the chart. The series has a property DataSeries which is the data being rendered and we enumerate over each of the points in this series. The chart series also has a relationship to the X & Y axes, and it is each axis which is responsible for the converting the X & Y components of our data into a ‘screen’ coordinate. This is performed via the (succinctly named!) GetDataValueAsRenderPositionWithoutZoom method (more on this later).

The list of points returned by the GetRenderPoints method are used to define a path (a path is defined by a geometry which is defined by one or more figures which themselves are defined by one or more segments … phew!). This path is styled and added to the RootZoomCanvas, with the attached property IsScaledPath set to true.

The net result of the above code is that we now have a fully functional line series which integrates with the Visiblox behaviours such as pan & zoom:

Now back to this ZoomCanvas …

The Visiblox charts have been optimised for performance and providing user interactions. A common requirement for interactive charts is the ability to pan and zoom. Both of these can be implemented by changing the range of the X and Y axes (for example a pan operation would result in a fixed offset being applied to the axis range), however this would result in the chart having to recomputed the location of all of each of the points being rendered. As an alternative, the Visiblox axes have a Zoom property which is handled by the ZoomCanvas directly. When a Zoom is applied, the required RenderTransforms are applied to the elements in the canvas, i.e. the series, resulting in a rapid update of chart. When using the ZoomCanvas you have to indicate whether the element added is based on a geometry, which should be transformed (i.e. a zoom makes the geometry appear bigger), or whether the element location should simply by updated to reflect the current zoom, for example, when you zoom in to a chart typically you would want the datapoints to maintain a fixed size.

Gergely Orosz describes the ZoomCanvas in more detail in his recent post on Panning & Zooming the charts.

To illustrate how this works, we will add points to our series which are not scaled as we zoom into the chart. The InvalidateInternal method is updated with the additional code to render the points, note that the ellipses have a RenderTransform applied so that their centre is anchored to the correct location on the ZoomCanvas:

protected override void InvalidateInternal()
{
  // ... code for rendering the path
 
  foreach (var renderPoint in renderPoints)
  {
    Ellipse el = new Ellipse()
    {
      Stroke = new SolidColorBrush(Colors.Black),
      StrokeThickness = 2.0,
      Fill = new SolidColorBrush(Colors.White),
      Width = 9,
      Height = 9,
      RenderTransform = new TranslateTransform()
      {
        X = -4,
        Y = -4
      }
    };
    RootZoomCanvas.Children.Add(el);
    ZoomCanvas.SetElementPosition(el, renderPoint);
  }
}

You can see the series in action below:

Creating a Smoothed Curve

So far we have created a simple line series; however the aim of this article is to create a smoothed line series. The Figure within our PathGeometry is currently composed of a number of straight line segments, in order to create a smoothed line we need to use curved segments. Silverlight and WPF both support Bézier curves and these can be used to specify a curved path geometry. A Bézier curve has four points, a start point, an end point and two control points which define the curvature:

In order to create a curved path for our series we need to determine suitable control points for our Bézier segments. After a bit of googling I stumbled upon an excellent article by Kerem Kat which describes how to use Bézier curves with Bing Maps. The article sourcecode has a method that will return the data points required to render a smoothed line as a Bézier Curve, using Catmull-Rom splines.

Modifying our series to use Bézier Curves, via the GetBezierPoints method described in the above article, is pretty straightforward:

protected override void InvalidateInternal()
{
  RootZoomCanvas.Children.Clear();
 
  Path path = new Path();
  PathGeometry geometry = new PathGeometry();
  PathFigure figure = new PathFigure();
 
  // obtain the render position of each point
  var renderPoints = GetRenderPoints();
 
  // create the bezier points as per:
  // http://www.codeproject.com/KB/silverlight/MapBezier.aspx
  PointCollection bezierPoints = GetBezierPoints(renderPoints, Tension);
 
  // create the figure composed of bezier segments
  figure.StartPoint = bezierPoints[0];
  for (int i = 1; i < bezierPoints.Count; i += 3)
  {
    figure.Segments.Add(new BezierSegment()
    {
      Point1 = bezierPoints[i],       
      Point2 = bezierPoints[i + 1],   
      Point3 = bezierPoints[i + 2]    
    });
  }
 
  geometry.Figures.Add(figure);
  path.Data = geometry;
  path.StrokeThickness = 2;
  path.Stroke = new SolidColorBrush(Colors.Black);
  ZoomCanvas.SetIsScaledPath(path, true);
 
  RootZoomCanvas.Children.Add(path);
}

Note the Tension property which is passed to the GetBezierPoints method, this is a property of Catmull-Rom splines which describes how ‘smoothed’ the line should be. The SplineSeries exposes this as a dependency property, as shown in the example below:

<vis:Chart x:Name="chart" LegendVisibility="Collapsed">
  <vis:Chart.Series>
    <local:SplineSeries Tension="{Binding Path=Value, ElementName=tensionSlider}"/>
  </vis:Chart.Series>
</vis:Chart>
 
<StackPanel Orientation="Horizontal" Grid.Row="1">
  <TextBlock Text="Tension:"/>
  <Slider x:Name="tensionSlider"
        Maximum="5.0" Minimum="1.0" Value="2.0" 
        Width="100"/>
  <TextBlock Text="{Binding Path=Value, ElementName=tensionSlider}"
              Width="18"/>
</StackPanel>

You can see the effect of changing the tension on the spline:

For a Bit Of Fun …

I thought it would be fun to visualise the Catmull-Rom spline construction by also rendering the control points for each Bézier curve. You can see the results below:

You can download the full sourcecode for the article here: VisibloxSplineSeries.zip

To run the examples you will also need to download the Visiblox charts from www.visiblox.com.

Regards, Colin E.