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

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!):

Get Microsoft Silverlight


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:

Get Microsoft Silverlight

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:

Get Microsoft Silverlight

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:

Get Microsoft Silverlight

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:

Get Microsoft Silverlight

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.

blog comments powered by Disqus