Fun With Telerik's Silverlight RadChartView

Whilst creating Indico 5 we decided to completely replace the now deprecated RadChart with RadChartView. We decided to do this in order to begin using the control the Telerik will be supporting and actively developing in the future in addition to all of Telerik's promoted advantages such as the speed and performance benefits.

With all of this in mind, we began replacing the 50 or so RadCharts utilised in Indico 4. Initially this seemed straightforward, but it didn't take long until we realised we were doing some reasonably complex things with our RadCharts which was not yet covered by the, at the time of writing, sparse RadChartView Silverlight documentation.

Several support tickets raised with Telerik have cleared up some of these issues and so, to help anyone else dealing with the same issues, I thought I would share some of the answers provided by Telerik. Most of these issues relate to using ChartView with the Model View View Model development pattern

Multiple Dynamic Series in MVVM

In ChartView Telerik completely changed the way data is bound, particularly in the case of creating multiple dynamic series using the MVVM pattern. For example a grouped bar chart.

At the time of creating the charts there was no documentation for this approach. We're pleased to see that documentation has now been added but thought we would explain our approach to this problem. It's a fairly intensive operation involving styles, converters and transforming flat data into nested data.

When creating a chart with Multiple Dynamic Series we make use of a SeriesProvider, such as the following:

<telerik:RadCartesianChart.SeriesProvider>
    <telerik:ChartSeriesProvider Source="{Binding Data}">
        <telerik:ChartSeriesProvider.SeriesDescriptors>
            <telerik:CategoricalSeriesDescriptor ItemsSourcePath="SubItems"
                                                ValuePath="Value"
                                                CategoryPath="Category"
                                                Style="{StaticResource DescriptorStyle}">
        </telerik:CategoricalSeriesDescriptor>
    </telerik:ChartSeriesProvider.SeriesDescriptors>
</telerik:ChartSeriesProvider>

The SeriesProvider handles creating the dynamic series for us (with a little bit more effort on our part). We set the data source for each series (SubItems) and the Category and Value paths.

<Style x:Key="DescriptorStyle" TargetType="chartView:BarSeries">
    <Setter Property="CombineMode" Value="Cluster" />
    <Setter Property="ShowLabels" Value="True" />
    <Setter Property="LegendSettings" Value="{Binding Label, Converter={StaticResource StringToLegendSettingsConverter}}" />
</Style>

The important part here is the LegendSettings property that handles each series label. The StringToLegendSettingsConverter is as follows:

public class StringToLegendSettingsConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return new SeriesLegendSettings() { Title = value.ToString() };
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Finally we need to perform a transform to take our flat data and turn it into the nested collection the series provider expects with the SubItems, Value, Category and Label fields;

First we need a data structure to hold the new data:

public class GroupedDataEntity<DataEntity>
{
    public int ID { get; set; }
    public string Label { get; set; }
    public int SeriesZIndex { get; set; }
    public ObservableCollection<DataEntity> SubItems { get; set; }
}

The SubItems collection will contain our Data Entity with the Category and Value fields.

Finally here is an example transformer using LINQ:

var query =
    from d in Data
    group d by new
    {
        d.SeriesID,
        d.Series
    }
        into myg
        select new GroupedDataEntity<DataEntity>
        {
            ID = myg.Key.SeriesID,
            Label = myg.Key.Series.ToString(),
            SubItems = new ObservableCollection<DataEntity>(myg)
        };

GroupedData = new ObservableCollection<GroupedDataEntity<DataEntity>>(query.AsEnumerable());

Data Templated ChartSeriesLabelDefinition

For a templated chart control we were looking to define the template for the ChartSeriesLabelDefinition, however found instead of the label being displayed the raw data was displayed instead. Telerik noted that due to performance reasons the label definitions don't contain any dependency properties and therefore bindings are not supported. This however can be resolved by wrapping the label template in a content presenter, such as:

<chartView:ChartSeriesLabelDefinition>
    <chartView:ChartSeriesLabelDefinition.Template>
        <DataTemplate>
            <ContentPresenter ContentTemplate="{Binding BottomStackLabelTemplate, RelativeSource={RelativeSource AncestorType=local:TemplatedChart}}" Content="{Binding}" />
        </DataTemplate>
    </chartView:ChartSeriesLabelDefinition.Template>
</chartView:ChartSeriesLabelDefinition>

Binding Chart Zoom & Determining Zoom Level

In ChartView Telerik released a great zooming feature, which allows for charts with large amounts of data points that can be panned and zoomed as required. Previously we used RadDataPager for this purpose, but have found this is now no longer needed (and a much better user experience).

Now we wanted to bind to the zoom and pan properties to set the zoom level based on bars shown (not available in ChartView) and to have a button to reset the zoom to its original state.

We initially bound to the Zoom property, using our number of bars zoom level, but found when the chart data was updated the zoom level was not reapplied. This resulted in the full chart data being shown.

After discussing with Telerik they noted that Zoom needed to be bound TwoWay. This resolved the issue;

Zoom="{Binding ZoomProperty, Mode=TwoWay}"

Customising axis label styles based on content (with scrolling)

We wanted to create stylized labels for data points based on the content for that data point - In our example we wanted to have a bar chart that would show red labels for data points that had a certain flag (in this case overspending dealers).

We found the example here, and it seemed to do exactly what we needed. However on implementation (and using the ChartView scroll bar) we found the label templates jumped between values, keeping the label pattern in the same onscreen position but jumping across labels. This appeared to be an issue with Container Recycling.

Telerik confirmed this was an issue (It's status can be tracked on PITS and as of writing is unfixed), and offered the following workaround template:

<local:TemplateConverter.Template2>
  <DataTemplate>
    <!--Template 2 - get text with {Binding}-->
  </DataTemplate>
</local:TemplateConverter.Template2>

public class TemplateConverter : IValueConverter
{
  public DataTemplate Template1
  {
      get;
      set;
  }
  public DataTemplate Template2
  {
      get;
      set;
  }

  public object Convert(object value, System.Type targetType, 
      object parameter, System.Globalization.CultureInfo culture)
  {
      //return Template1 or Template2 based on value of 'value'
  }

  public object ConvertBack(object value, System.Type targetType, 
      object parameter, System.Globalization.CultureInfo culture)
  {
      throw new System.NotImplementedException();
  }
}

As an extra tip, we found the best way to pass the conditional parameter was to use a special text character at the end of the label string, and if it exists apply the template and remove the character.

Binding Series Columns in RadPieChart

We had an issue with binding data to a RadPieChart - previously the RadChart allowed binding to pivoted data - i.e. a column for each pie segment type (We use this data format as we work with predetermined values). The problem was that RadPieChart does not support this out of the box.

In order to fix this Telerik suggested we create a custom data type that the pie chart supported and to transform our data accordingly. The resulting code is not particularly elegant but it works, and is as follows;

Custom data type:

public class PieDataPoint
{
    public int Value { get; set; }
    public string Label { get; set; }
}

Transformer code (as an added bonus we've added code to fix the issue with an empty dataset showing no pie and a legend, instead of "No data series")

if (items != null)
{
    if (items.Any())
    {
        // Not ideal... transform the collection into one that the Pie chart can bind to.
        var result = new ObservableCollection<PieDataPoint>
                {
                    new PieDataPoint() {Label = "Item1", Value = items[0].Item1 ?? 0},
                    new PieDataPoint() {Label = "Item2", Value = items[0].Item2 ?? 0},
                    //...
                };

        // If all returned descriptors are 0 return a blank collection to force "no data series", else return the result collection.
        PieData = result.All(x => x.Value == 0) ? new ObservableCollection<PieDataPoint>() : result;
    }
}

Adding hand cursor to clickable elements

Here's a simple one, but not available out of the box - If a chart item is clickable, then the cursor should change to "hand" over clickable elements, alas not in ChartView! Here's the fix;

When defining a series that is clickable, set:

IsHitTestVisible="True" Cursor="Hand"