Colin Eberhardt's Adventures in WPF

Ineffective Data Visualisation … and how to fix it

April 30th, 2010

This blog post looks at a recently published set of charts in a UK newspaper and how they fail to help in the comprehension of the data which they visualise. I will also look at much more effective ways of displaying this same data.

At Scott Logic we tend to spend quite a bit of our time thinking about the effective visualisation of data. In the financial sector data abounds, with stock prices changing every second, traders and analysts have a lot of data at their disposal. Without methods to analyse and visualise this data it is easy to gets lost in the sheer quantity. For this reason, the works of Edward Tufte and Stephen Few are often passed round the office!

With the UK General election looming, statistics and trends are a common feature in our news. Unfortunately these seems to lead to a whole slew of charts and graphics which succeed in their artistry but fail miserably in helping the reader understand the data which the graphics represent.

Just this morning I was reading an article in the Metro newspaper about the changes in party support over the past week’s opinion polls and the voting habits of different age groups. The article was supported by the following graphic:

One of the key ideas behind the charting and visualising of data is to allow the reader to rapidly digest the data, spot trends, understand relationships, etc… Unfortunately, the graphics above fail miserably in this respect. Here are some of the faults I spotted:

(1) Chart title – the main chart title relates to the chart on the right, but not to the chart on the left.

(2) Choice of colours - if you look at the datapoints on the right-hand chart it is not easy to determine which party they relate to due to a poor choice of colour, peach and salmon?!

(3) Trends are hidden - the main purpose of the right hand chart is to illustrate the trends in party support with relation to age. To do this you have to hunt for the same coloured point from one age band to the next.

(4) Gridlines – the right-hand chart has labels every 5 percent point, but gridlines every 2 points. This means that there is not a gridline for each label, this makes it very hard to determine the actual value of each datapoint.

(5) Doughnut – the doughnut (i.e. the stylised pie-chart with a yummy hole) has a couple of problems, which week does it represent the split in party support for? this week? last week? Also, which is the bigger pie piece, Lib. Dem. or Conservative? It is impossible to tell without reaching for your protractor (I seem to have left mine at home today).

(6) Arbitrary graphics – I cannot see any reason, other than artistic licence, for the vertical highlights on the right-hand chart. This is misleading, it draws the eye to these areas of the chart with the expectation that they are highlighted for some reason.

(7) Change not visualised – the change in support from last week to this week is not visualised in any way, it is presented in tabular form. This means that the reader might miss important information, for example, a 10 percent point raise from 10 to 20 is clearly more significant than a rise from 70 to 80, this is made quite obvious if we visualise the change.

(8) Units – the indication of units is quite distant from the data.

I am sure there are more problems … if you spot any others, leave a comment.

So, let’s see if we can rectify some of these issues. Starting with the chart on the right, its main purpose is to illustrate the relationship between age group and party support. In this case it is vital that the reader of this chart can easily navigate from the datapoint which indicated Conservative party support (for example) in one age range the next. With this in a mind, a line chart is much more appropriate and the trends become immediately visible:

Note also the colours, these are no longer arbitrarily assigned. Each political party has a party colour which, if used, allows most people to instantly determine the party each line relates to. The gridlines are also more sensibly placed and we have lost the ‘artistic’ highlights. Finally, the Y axis starts at zero, this allow the reader to instantly see the scale of the differences between the popularity figures without having to read the axis range.

Now, let’s turn our attention to the doughnut and the accompanying table. The reader should be able to determine two key pieces of information from these, (1) The relative popularity of each party and (2) The change in popularity since last week. It would be ideal if the two could be combined so that the reader can also compare the scale of this change with the overall difference in popularity. In order to allow this, it is much better to display the information in a single chart:

With the above chart we can see at-a-glance the relative popularity of each party again displayed in party colours. I must admit it took me a little while to work out how to indicate which columns represented this week’s figures and which were last week’s. I tried using variations in the column intensity, but this is a hard concept to indicate via a key, I also added small labels, but this just complicates and clutters. Finally I realised that by adding a pattern I could maintain the party colour, yet clearly relate the columns for the previous week (This makes use of the Gestalt Principle of Similarity). Unfortunately Excel 2007, which I used to create these charts, does not support patterns, however I found this excellent add-in from Andy Pope, and I thoroughly recommend it.

I think the two charts I have presented are much clearer than those in the original graphics from the newspaper article. However, a direct comparison between the two would not be entirely fair. The graphics used in the media often have further constraints imposed on them, (1) They are often restricted in size, having to fit within a fixed page size layout, (2) They should be eye-catching and visually appealing, drawing a potential reader towards the article.

With this in mind, I have re-worked the graphs above into the same layout and size as the originals. I have even added drop-shadows for visual appeal …

I think the above is a good compromise between providing an artistically pleasing graphic whilst still allowing the reader to understand the data (and from there spot trends etc…).

Regards, Colin E.

Update: Thanks to Graham Odds for a few extra ideas about tidying up the final charts.

Adding a Location Crosshair to Silverlight charts (again!)

March 23rd, 2009

Silverlight is moving fast. Really fast. The recent MIX09 conference saw the release of Silverlight 3 (Beta) and also a new release of the Silverlight Toolkit. All this change is making it hard for us bloggers to keep up!

Just over a month ago I posted an article on this blog about how to add a location crosshair to the Silverlight Toolkit Charts. This blog post briefly discusses how the charts have changed with the release last week, and updates the code accordingly.

If you want to know how the general approach works, I would recommend reading the previous blog post, this post serves as an update and discussion on how the chart template has changed.

There have been a number of minor changes to the charts that affect my previous implementation, including a change of namespace and orientation type on the chart axes. However, the biggest change is in the template of the chart control itself. Previously the chart template was structured via a Grid as shown below:

<ControlTemplate TargetType="charting:Chart">
    <Grid Name="ChartArea" Style="{TemplateBinding ChartAreaStyle}">
 
        <!-- ##### chart control adds column / row definitions and axes here #### -->
 
        <Grid Height="250" x:Name="PlotArea" Style="{TemplateBinding PlotAreaStyle}">
 
            <!-- the standard chart template components -->
            <Grid x:Name="GridLinesContainer" />
            <Grid x:Name="SeriesContainer"/>
            <Border BorderBrush="#FF919191" BorderThickness="1" />
 
            <!-- ##### I added cross hair and legend here #### -->
        </Grid>
    </Grid>
</ControlTemplate>

After loading the template the chart adds the axes and the appropriate column / row definitions. This leaves us free to add new visual content withing the ‘PlotArea’ grid. Inspecting the visual tree, with Silverlight Spy, it look like the following:

visualtree-before

The content I added in order to construct a crosshair and legend are highlighted in red.

The March release of the Silverlight toolkit modifies the way in which axes are added to the chart by the introduction of a new layout panel, the EdgePanel (think DockPanel!). This panel allows you to locate (or dock) elements at one of the four edges of a panel or the panel centre. The modified control template is show below:

<ControlTemplate TargetType="charting:Chart" x:Key="ChartTemplate">
    <Grid x:Name="ChartRoot" Style="{TemplateBinding PlotAreaStyle}">
 
        <chartingprimitives:EdgePanel x:Name="ChartArea" Style="{TemplateBinding ChartAreaStyle}">                                   
 
            <Grid Canvas.ZIndex="1" Style="{TemplateBinding PlotAreaStyle}" />
            <Border Canvas.ZIndex="1" BorderBrush="#FF919191" BorderThickness="1" />
 
            <!-- ##### I added cross hair and legend here #### -->
 
            <!-- ##### chart control adds column / row definitions and axes here #### -->
 
        </chartingprimitives:EdgePanel>
    </Grid>
</ControlTemplate>

I have indicated the location where I add my own visual content and also the location where the chart control adds the axes. The resulting visual tree is shown below:

visualtree-after

The significant difference here is that all the components of our chart are contained within the same EdgePanel, with their location dictated by their EdgePanel.Edge property. One problem this does introduce for us is that of Z-ordering, previously our additional content was top of the stack, now it sits somewhere in between. Fortunately EdgePanel honours the Canvas.ZIndex attached property allowing us to push our content to the top. The modified template is given below in full:

<ControlTemplate TargetType="charting:Chart" x:Key="ChartTemplate">
    <Grid x:Name="ChartRoot" Style="{TemplateBinding PlotAreaStyle}">
 
        <chartingprimitives:EdgePanel x:Name="ChartArea" Style="{TemplateBinding ChartAreaStyle}">                                   
 
            <Grid Canvas.ZIndex="-1" Style="{TemplateBinding PlotAreaStyle}" />
            <Border Canvas.ZIndex="3" BorderBrush="#FF919191" BorderThickness="1" />
 
            <!-- a location crosshair -->
            <Grid Name="CrosshairContainer" Canvas.ZIndex="1" Background="Transparent"
                 MouseMove="CrosshairContainer_MouseMove" MouseEnter="CrosshairContainer_MouseEnter"
                 MouseLeave="CrosshairContainer_MouseLeave" >
                <Grid Name="Crosshair">
                    <Line Name="Vertical" X1="{Binding Path=X}" Y1="0" X2="{Binding Path=X}" Y2="400" Stroke="Black"/>
                    <Line Name="Horizontal" X1="0" Y1="{Binding Path=Y}" X2="400" Y2="{Binding Path=Y}" Stroke="Black"/>
                </Grid>
            </Grid>
 
            <!-- a location 'legend' -->
            <Border Canvas.ZIndex="2" Name="LocationIndicator" Visibility="Collapsed" Style="{StaticResource LocationLegendStyle}">
                <StackPanel Orientation="Horizontal" Margin="5">
                    <TextBlock Text="Location: "/>
                    <TextBlock Text="{Binding Path=Key,
                                Converter={StaticResource FormattingConverter}, ConverterParameter=hh:mm:ss}"/>
                    <TextBlock Text=", "/>
                    <TextBlock Text="{Binding Path=Value,
                                Converter={StaticResource FormattingConverter}, ConverterParameter=0.00}"/>
                </StackPanel>
            </Border>
 
        </chartingprimitives:EdgePanel>
    </Grid>
</ControlTemplate>

One more subtle point, the event handlers for MouseMove, MouseLeave etc.. when associated with a Grid only work if the background is not null, hence the need for a transparent background. I have no idea why, believe me … it just works!

One final important change is the method used to translate points from screen coordinates into a position within the chart coordinate system. Previously I used a method called GetPlotAreaCoordinateValueRange on the ‘hidden IRangeAxis interface. This has now been renamed to GetValueAtPosition and returns and takes a UnitValue type as its input (presumably for API consistency with the pie charts). Here is the code:

/// <summary>
/// Transforms the supplied position on the plot area grid into a point within
/// the plot area coordinate system
/// </summary>
private KeyValuePair<DateTime, double> GetPlotAreaCoordinates(Point position)
{
    IComparable yAxisHit = ((IRangeAxis)YAxis).GetValueAtPosition(
        new UnitValue(PlotArea.ActualHeight - position.Y, Unit.Pixels));
 
    IComparable xAxisHit = ((IRangeAxis)XAxis).GetValueAtPosition(
        new UnitValue(position.X, Unit.Pixels));
 
    return new KeyValuePair<DateTime, double>((DateTime)xAxisHit, (double)yAxisHit);
}

With these changes in place, our crosshair is fully functional once more:

… until the next release of the toolkit ;-)

You can download the project sourcecode: sllinechartcrosshairupdated.zip.

Regards, Colin E.

Adding a Location Crosshair to Silverlight Charts

February 4th, 2009
UPDATE – The March09 update of the Silverlight toolkit is incompatible with the code detailed below. For an up-to-date version see the following blog post.

This blog post describes how to add a location crosshair to your Silverlight charts as shown below:

The chart itself is rendered using the charting component of the recently release Silverlight Toolkit. Creating and displaying a simple line chart is as simple as referencing the correct silverlight toolkit namespaces and placing a chart with an associated lineseries into the XAML for your page:

<charting:Chart Name="Chart" PlotAreaStyle="{StaticResource PlotAreaStyle}">
    <charting:LineSeries
		IndependentValueBinding="{Binding Path=Key}"
		DependentValueBinding="{Binding Path=Value}"/>
</charting:Chart>

In this example, the data is provided in the form of an XML file as shown in the snippet below:

<?xml version="1.0" encoding="UTF-8"?>
<history generated="2009/02/03 10:51:02 GMT+0000">
  <dataPoints>
    <dataPoint date="2009/02/03 08:01:00 GMT+0000" change="21.61" changePercent="0.36" value="6073.99"/>
    <dataPoint date="2009/02/03 08:02:45 GMT+0000" change="16.11" changePercent="0.27" value="6068.49"/>
    ...
    <dataPoint date="2009/02/03 10:50:45 GMT+0000" change="0.4" changePercent="0.01" value="6052.78"/>
  </dataPoints>
</history>

This data shows the performance of the FTSE 100 index on a relatively uneventful morning. The XML file is read using LINQ to XML into a collection of KeyValuePairs. The LineSeries’ ItemsSource is set to the result of this query, with the bindings in the above XAML binding the Key and Value properties of each KeyValuePair instance.

protected LineSeries LineSeries
{
    get
    {
        return ((LineSeries)Chart.Series[0]);
    }
}
 
public Page()
{
    InitializeComponent();
 
    XDocument doc = XDocument.Load("chartData.xml");
    var elements = from dataPoint in doc.Descendants("dataPoint")                           
                   select new KeyValuePair<DateTime, double>
                   (
                       DateTime.Parse(dataPoint.Attribute("date").Value.Substring(0, 19)),
                       Double.Parse(dataPoint.Attribute("value").Value)
                   );
 
    LineSeries.ItemsSource = elements;
}

This results in the following (rather ugly!) chart:

ftse100

Whilst the background fade effect and line series datapoints can be modified by styling the chart, the removal of unwanted elements such as the legend require us to delve into the the Chart’s control template. The modified control template is given below:

<charting:Chart.Template>
    <ControlTemplate TargetType="charting:Chart">
        <Grid Name="ChartArea" Style="{TemplateBinding ChartAreaStyle}">
 
            <!-- NOTE: the chart legend and title have been removed -->
            <Grid Height="250" x:Name="PlotArea" Style="{TemplateBinding PlotAreaStyle}"
                  MouseMove="PlotArea_MouseMove" MouseEnter="PlotArea_MouseEnter" MouseLeave="PlotArea_MouseLeave">
 
                <!-- the standard chart template components -->
                <Grid x:Name="GridLinesContainer" />
                <Grid x:Name="SeriesContainer"/>
                <Border BorderBrush="#FF919191" BorderThickness="1" />
 
                <!-- a location crosshair -->
                <Grid Name="Crosshair" Visibility="Collapsed">
                    <Line Name="Vertical" X1="{Binding Path=X}" Y1="0" X2="{Binding Path=X}" Y2="250" Stroke="Black"/>
                    <Line Name="Horizontal" X1="0" Y1="{Binding Path=Y}" X2="400" Y2="{Binding Path=Y}" Stroke="Black"/>
                </Grid>
 
                <!-- a location 'legend' -->
                <Border Name="LocationIndicator" Visibility="Collapsed" Style="{StaticResource LocationLegendStyle}">
                    <StackPanel Orientation="Horizontal" Margin="5">
                        <TextBlock Text="Location: "/>
                        <TextBlock Text="{Binding Path=Key}"/>
                        <TextBlock Text=", "/>
                        <TextBlock Text="{Binding Path=Value}"/>
                    </StackPanel>
                </Border>
            </Grid>
        </Grid>
    </ControlTemplate>
</charting:Chart.Template>

In the above template you can see a couple of Grids, one named GridLinesContainer, and the other names SeriesContainer. When constructing the chart, the chart control will navigate the visual tree constructed from its control template to find elements of these names. It will then add the grid lines and series to these containers accordingly. This gives us the freedom to modify the chart’s control template whilst still allowing it to function normally. In the above template I have added two new elements, a cross hair and a location indicator. I have also added handlers for a few of the mouse events on the plot area.

The mouse move event handler in the code behind finds the current mouse position within the PlotArea grid. Firstly, the coordinates of this point are found within the chart coordinate system (more on this later), following this, the DataContexts for the Crosshair and PlotArea are set to the mouse position and location within the chart coordinate system respectively. Looking in the above XAML we can see that the Crosshair contains a pair of lines which are bound to the X and Y properties of their DataContext, ensuring that the two lines intersect at the current mouse location. The LocationIndicator will inherit the PlotArea’s DataContext, allowing it to output the current location in the chart coordinate system.

private void PlotArea_MouseMove(object sender, MouseEventArgs e)
{
    if (LineSeries.ItemsSource == null)
        return;
 
    Point mousePos = e.GetPosition(PlotArea);
    KeyValuePair<DateTime, double> crosshairLocation = GetPlotAreaCoordinates(mousePos);
 
    PlotArea.DataContext = crosshairLocation;
    Crosshair.DataContext = mousePos;
    PlotArea.Cursor = Cursors.None;
}
 
protected Grid PlotArea
{
    get { return ChartArea.FindName("PlotArea") as Grid; }
}
 
protected Grid Crosshair
{
    get { return ChartArea.FindName("Crosshair") as Grid; }
}
 
protected Grid ChartArea
{
    get
    {
        // chart area is within a different namescope to this page, therefore
        // we must navigate the visual tree to locate it
        return VisualTreeHelper.GetChild(Chart, 0) as Grid;
    }
}

One thing worth noting in the above code is that way in which the PlotArea and Crosshair elements are located. Usually it is possible to simply name elements within your XAML using their Name attribute then refer to them directly in the code-behind or locate them via the DependencyObject.FindName method. However, FindName relies on the named element being in the same namescope. Control template are in a different namescope to the XAML page which they are defined within, therefore we have to navigate the visual tree to find the root element of the chart control, then invoke FindName from there. See the MSDN page on XAML namescopes for more details.

The GetPlotAreaCoordinates method is given below:

private KeyValuePair<DateTime, double> GetPlotAreaCoordinates(Point position)
{
    Range<IComparable> yAxisHit = ((IRangeAxis)YAxis).GetPlotAreaCoordinateValueRange(PlotArea.Height - position.Y);
    Range<IComparable> xAxisHit = ((IRangeAxis)XAxis).GetPlotAreaCoordinateValueRange(position.X);
 
    return new KeyValuePair<DateTime, double>((DateTime)xAxisHit.Minimum, (double)yAxisHit.Minimum);
}

The silverlight chart axes actually provides methods which can be used to translate from screen coordinates to chart coordinates via the IRangeAxis interface, however they are hidden by explicit interface implementation of these interface methods by the concrete axis classes. It is a shame that such useful methods are hidden! I have never really seen the value of explicit interface implementation.

In conclusion, this blog post has shown how simple it is to add a crosshair to your silverlight linecharts. What I personally find interesting is the way in which you can not only customise the appearance of Silverlight (and WPF) controls, but also add behaviour without the need to subclass the controls themselves. I think that this makes life easier for the users of Silverlight/WPF controls, in that they can use the same techniques to extend any control rather than relying on the control having built in extension points (it is often a source of frustration when developing with Windows Forms (or similar technologies) when you wish to customise some aspect of an existing control, however the control author has not provided events, or virtual methods for this purpose). It also of course makes life easier for the control developer in that they do not have to spend so much time thinking about how their control might be extended and adapted by its users.

You can download a visual studio project with the code from this blog post, sllinechartcrosshair.zip.

Regards, Colin E.