June 2010

Volume 25 Number 06

Test Run - Generating Graphs with WPF

By James McCaffrey | June 2010

James McCaffreyGenerating a graph from a set of test-related data is a common software-development task. In my experience, the most common approach is to import data into an Excel spreadsheet, then produce the graph manually using the Excel built-in graphing features. This works well in most situations, but if the underlying data changes frequently, creating graphs by hand can quickly become tedious. In this month’s column, I’ll show you how to automate the process using Windows Presentation Foundation (WPF) technology. To see where I’m heading, look at Figure 1. The graph shows a count of open versus closed bugs by date, and it was generated on the fly using a short WPF program that reads data from a simple text file.

Figure 1 Programmatically Generated Bug-Count Graph
Figure 1 Programmatically Generated Bug-Count Graph

The open bugs, represented by red circles on the blue line, increase rapidly near the beginning of the development effort, then trail off over time—information that might be useful when estimating a zero-bug bounce date. The closed bugs (the triangular markers on the green line) steadily increase.

But while the information may be useful, in production environments development resources are often limited, and manually generating such a graph might not be worth the effort. But using the technique I’ll explain, creating graphs like this is quick and easy.

In the following sections, I’ll present and describe in detail the C# code that generated the graph in Figure 1. This column assumes you have intermediate-level knowledge of C# coding and a very basic familiarity with WPF. But even if you’re new to both, I think you’ll be able to follow the discussion without too much difficultly. I’m confident you’ll find the technique an interesting and useful addition to your skill set.

Setting up the Project

I started by launching Visual Studio 2008 and creating a new C# project using the WPF Application template. I selected the .NET Framework 3.5 library from the drop-down control in the upper right-hand area of the New Project dialog box. I named my project BugGraph. Although you can programmatically generate graphs using WPF primitives, I used the convenient DynamicDataDisplay library developed by a Microsoft Research lab.

You can download the library for free from the CodePlex open source hosting site at codeplex.com/dynamicdatadisplay. I saved my copy in the root directory of my BugGraph project, then added a reference to the DLL in my project by right-clicking on the project name, selecting the Add Reference option and pointing to the DLL file in my root directory.

Next, I created my source data. In a production environment, your data could be located in an Excel spreadsheet, a SQL database or an XML file. For simplicity, I used a simple text file. In the Visual Studio Solution Explorer window, I right-clicked on my project name and selected Add | New Item from the context menu. I then chose the Text File item, renamed the file to BugInfo.txt and clicked the Add button. Here’s the dummy data:

01/15/2010:0:0 02/15/2010:12:5 03/15/2010:60:10 04/15/2010:88:20 05/15/2010:75:50 06/15/2010:50:70 07/15/2010:40:85 08/15/2010:25:95 09/15/2010:18:98 10/15/2010:10:99

The first colon-delimited field in each line holds a date, the second contains the number of open bugs on the associated date and the third field shows the number of closed bugs. As you’ll see shortly, the DynamicDataDisplay library can deal with most types of data.

Next I double-clicked on the file Window1.xaml to load the UI definitions for the project. I added a reference to the graphing library DLL and slightly modified the default Height, Width and Background attributes of the WPF display area, as follows:

xmlns:d3="https://research.microsoft.com/DynamicDataDisplay/1.0" 
Title="Window1" WindowState="Normal" Height="500" Width="800" Background="Wheat">

After that, I added the key plotting object, shown in Figure 2.

Figure 2 Adding the Key Plotting Object

<d3:ChartPlotter Name="plotter" Margin="10,10,20,10">
  <d3:ChartPlotter.HorizontalAxis>
    <d3:HorizontalDateTimeAxis Name="dateAxis"/>
  </d3:ChartPlotter.HorizontalAxis>
  <d3:ChartPlotter.VerticalAxis>
    <d3:VerticalIntegerAxis Name="countAxis"/>
  </d3:ChartPlotter.VerticalAxis>

  <d3:Header FontFamily="Arial" Content="Bug Information"/>
  <d3:VerticalAxisTitle FontFamily="Arial" Content="Count"/>
  <d3:HorizontalAxisTitle FontFamily="Arial" Content="Date"/>
</d3:ChartPlotter>

The ChartPlotter element is the main display object. In the definition for it, I added declarations for a horizontal date axis and a vertical integer axis. The default axis type for the DynamicDataDisplay library is a number with a decimal, that is type double in C# terms; no explicit axis declaration is necessary for that type. I also added a header title declaration and axis title declarations. Figure 3 shows my design so far.

Figure 3 BugGraph Program Design
Figure 3 BugGraph Program Design

Going to the Source

Once I’d configured the static aspects of my project, I was ready to add the code that would read the source data and programmatically generate my graph. I double-clicked on Window1.xaml.cs in the Solution Explorer window to load the C# file into the code editor. Figure 4 lists the entire source code for the program that generated the graph in Figure 1.

Figure 4 Source Code for the BugGraph Project

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media; // Pen

using System.IO;
using Microsoft.Research.DynamicDataDisplay; // Core functionality
using Microsoft.Research.DynamicDataDisplay.DataSources; // EnumerableDataSource
using Microsoft.Research.DynamicDataDisplay.PointMarkers; // CirclePointMarker

namespace BugGraph
{
  public partial class Window1 : Window
  {
    public Window1()
    {
      InitializeComponent();
      Loaded += new RoutedEventHandler(Window1_Loaded);
    }

    private void Window1_Loaded(object sender, RoutedEventArgs e)
    {
      List<BugInfo> bugInfoList = LoadBugInfo("..\\..\\BugInfo.txt");

      DateTime[] dates = new DateTime[bugInfoList.Count];
      int[] numberOpen = new int[bugInfoList.Count];
      int[] numberClosed = new int[bugInfoList.Count];

      for (int i = 0; i < bugInfoList.Count; ++i)
      {
        dates[i] = bugInfoList[i].date;
        numberOpen[i] = bugInfoList[i].numberOpen;
        numberClosed[i] = bugInfoList[i].numberClosed;
      }

      var datesDataSource = new EnumerableDataSource<DateTime>(dates);
      datesDataSource.SetXMapping(x => dateAxis.ConvertToDouble(x));

      var numberOpenDataSource = new EnumerableDataSource<int>(numberOpen);
      numberOpenDataSource.SetYMapping(y => y);

      var numberClosedDataSource = new EnumerableDataSource<int>(numberClosed);
      numberClosedDataSource.SetYMapping(y => y);

      CompositeDataSource compositeDataSource1 = new
        CompositeDataSource(datesDataSource, numberOpenDataSource);
      CompositeDataSource compositeDataSource2 = new
        CompositeDataSource(datesDataSource, numberClosedDataSource);

      plotter.AddLineGraph(compositeDataSource1,
        new Pen(Brushes.Blue, 2),
        new CirclePointMarker { Size = 10.0, Fill = Brushes.Red },
        new PenDescription("Number bugs open"));

      plotter.AddLineGraph(compositeDataSource2,
        new Pen(Brushes.Green, 2),
        new TrianglePointMarker { Size = 10.0,
          Pen = new Pen(Brushes.Black, 2.0),
            Fill = Brushes.GreenYellow },
        new PenDescription("Number bugs closed"));

      plotter.Viewport.FitToView();

    } // Window1_Loaded()

    private static List<BugInfo> LoadBugInfo(string fileName)
    {
      var result = new List<BugInfo>();
      FileStream fs = new FileStream(fileName, FileMode.Open);
      StreamReader sr = new StreamReader(fs);
     
      string line = "";
      while ((line = sr.ReadLine()) != null)
      {
        string[] pieces = line.Split(':');
        DateTime d = DateTime.Parse(pieces[0]);
        int numopen = int.Parse(pieces[1]);
        int numclosed = int.Parse(pieces[2]);
        BugInfo bi = new BugInfo(d, numopen, numclosed);
        result.Add(bi);
      }
      sr.Close();
      fs.Close();
      return result;
    }

  } // class Window1

  public class BugInfo {
  public DateTime date;
  public int numberOpen;
  public int numberClosed;

  public BugInfo(DateTime date, int numberOpen, int numberClosed) {
    this.date = date;
    this.numberOpen = numberOpen;
    this.numberClosed = numberClosed;
  }

}} // ns

I deleted the unnecessary using namespace statements (such as System.Windows.Shapes), which were generated by the Visual Studio template. Then I added using statements to three namespaces from the DynamicDataDisplay library so I wouldn’t have to fully qualify their names. Next, in the Window1 constructor I added an event for the main program-defined routine:

Loaded += new RoutedEventHandler(Window1_Loaded);

Here’s how I began the main routine:

private void Window1_Loaded(object sender, RoutedEventArgs e)
{
  List<BugInfo> bugInfoList = LoadBugInfo("..\\..\\BugInfo.txt");
  ...

I declared a generic list object, bugInfoList, and populated the list with the dummy data in the file BugInfo.txt by using a program-defined helper method named LoadBugInfo. To organize my bug information, I declared a tiny helper class—BugInfo—as Figure 5 shows.

Figure 5 The Helper Class BugInfo

public class BugInfo {
  public DateTime date;
  public int numberOpen;
  public int numberClosed;

  public BugInfo(DateTime date, int numberOpen, int numberClosed) {
    this.date = date;
    this.numberOpen = numberOpen;
    this.numberClosed = numberClosed;
  }
}

I declared the three data fields as type public for simplicity, rather than as type private combined with get and set Properties. Because BugInfo is just data, I could’ve used a C# struct instead of a class. The LoadBugInfo method opens the BugInfo.txt file and iterates through it, parsing each field, then instantiates a BugInfo object and stores each BugInfo object into a result List, as shown in Figure 6.

Figure 6 The LoadBugInfo Method

private static List<BugInfo> LoadBugInfo(string fileName)
{
  var result = new List<BugInfo>();
  FileStream fs = new FileStream(fileName, FileMode.Open);
  StreamReader sr = new StreamReader(fs);
     
  string line = "";
  while ((line = sr.ReadLine()) != null)
  {
    string[] pieces = line.Split(':');
    DateTime d = DateTime.Parse(pieces[0]);
    int numopen = int.Parse(pieces[1]);
    int numclosed = int.Parse(pieces[2]);
    BugInfo bi = new BugInfo(d, numopen, numclosed);
    result.Add(bi);
  }
  sr.Close();
  fs.Close();
  return result;
}

Rather than reading and processing each line of the data file, I could have read all the lines into a string array using the File.ReadAllLines method. Notice that both to keep the size of my code small and for clarity, I omitted the normal error-checking you’d perform in a production environment.

Next I declared and assigned values to three arrays, as you can see in Figure 7.

Figure 7 Building Arrays

DateTime[] dates = new DateTime[bugInfoList.Count];
  int[] numberOpen = new int[bugInfoList.Count];
  int[] numberClosed = new int[bugInfoList.Count];

  for (int i = 0; i < bugInfoList.Count; ++i)
  {
    dates[i] = bugInfoList[i].date;
    numberOpen[i] = bugInfoList[i].numberOpen;
    numberClosed[i] = bugInfoList[i].numberClosed;
  }
  ...

When working with the DynamicDataDisplay library, organizing the display data into a set of one-dimensional arrays is often convenient. As an alternative to my program design, which read data into a list object and then transferred the list data into arrays, I could have read data directly into arrays.

Next I converted my data arrays into special EnumerableDataSource types:

var datesDataSource = new EnumerableDataSource<DateTime>(dates);
datesDataSource.SetXMapping(x => dateAxis.ConvertToDouble(x));

var numberOpenDataSource = new EnumerableDataSource<int>(numberOpen);
numberOpenDataSource.SetYMapping(y => y);

var numberClosedDataSource = new EnumerableDataSource<int>(numberClosed);
numberClosedDataSource.SetYMapping(y => y);
...

For the DynamicDataDisplay library, all data to be graphed must be in a uniform format. I just passed the three arrays of data to the generic EnumerableDataSource constructor. Additionally, the library must be told which axis, x or y, is associated with each data source. The SetXMapping and SetYMapping methods accept method delegates as arguments. Rather than define explicit delegates, I used lambda expressions to create anonymous methods. The DynamicDataDisplay library’s fundamental-axis data type is double. The SetXMapping and SetYMapping methods map my particular data type to type double.

On the x-axis, I used the ConvertToDouble method to explicitly convert DateTime data into type double. On the y-axis, I simply wrote y => y (read as “y goes to y”) to implicitly convert the input int y to the output double y. I could have been explicit with my type mapping by writing SetYMapping(y => Convert.ToDouble(y). My choices of x and y for the lambda expressions’ parameters were arbitrary—I could have used any parameter names.

The next step was to combine the x-axis and y-axis data sources:

CompositeDataSource compositeDataSource1 = new
  CompositeDataSource(datesDataSource, numberOpenDataSource);

CompositeDataSource compositeDataSource2 = new
  CompositeDataSource(datesDataSource, numberClosedDataSource);

...

The screenshot in Figure 1 shows two data series—the number of open bugs and the number of closed bugs—plotted on the same graph. Each composite data source defines a data series, so here I needed two individual data sources—one for the number of open bugs and one for the number of closed bugs. With the data all prepared, a single statement actually plotted the data points:

plotter.AddLineGraph(compositeDataSource1,
  new Pen(Brushes.Blue, 2),
  new CirclePointMarker { Size = 10.0, Fill = Brushes.Red },
  new PenDescription("Number bugs open"));

...

The AddLineGraph method accepts a CompositeDataSource, which defines the data to be plotted, along with information about exactly how to plot it. Here I instructed the plotter objectnamed plotter (defined in the Window1.xaml file) to do the following: draw a graph using a blue line of thickness 2, place circular markers of size 10 that have red borders and red fill, and add the series title Number bugs open. Neat! As one of many alternatives, I could have used

plotter.AddLineGraph(compositeDataSource1, Colors.Red, 1, "Number Open")

to draw a thin red line with no markers. Or I could have created a dashed line instead of a solid line:

Pen dashedPen = new Pen(Brushes.Magenta, 3);
dashedPen.DashStyle = DashStyles.DashDot;
plotter.AddLineGraph(compositeDataSource1, dashedPen,
  new PenDescription("Open bugs"));

My program finished by plotting the second data series:

... 
    plotter.AddLineGraph(compositeDataSource2,
    new Pen(Brushes.Green, 2),
    new TrianglePointMarker { Size = 10.0,
      Pen = new Pen(Brushes.Black, 2.0),
      Fill = Brushes.GreenYellow },
    new PenDescription("Number bugs closed"));

  plotter.Viewport.FitToView();

} // Window1_Loaded()

Here I instructed the plotter to use a green line with triangular markers that have a black border and a green-yellow fill. The FitToView method scales the graph to the size of the WPF window.

After instructing Visual Studio to build the BugGraph project, I got a BugGraph.exe executable, which can be launched manually or programmatically at any time. I can update the underlying data by simply editing the BugInfo.txt file. Because the entire system is based on .NET Framework code, I can easily integrate graphing capability into any WPF project without having to deal with cross-technology issues. And there’s a Silverlight version of the DynamicDataDisplay library so I can add programmatic graphing to Web applications, too.

A Scatter Plot

The technique I presented in the previous section can be applied to any kind of data, not just test-related data. Let’s take a brief look at another simple but rather impressive example. The screenshot in Figure 8 shows 13,509 U.S. cities.

Figure 8 Scatter Plot Example
Figure 8 Scatter Plot Example

You can probably identify where Florida, Texas, Southern California and the Great Lakes are. I obtained the data for the scatter plot from a library of data intended for use with the traveling salesman problem (www.iwr.uni-heidelberg.de/groups/comopt/software/TSPLIB95), one of the most famous and widely studied topics in computer science. The file I used, usa13509.tsp.gz, looks like:

NAME : usa13509
(other header information)
1 245552.778 817827.778
2 247133.333 810905.556
3 247205.556 810188.889
...

13507 489663.889 972433.333
13508 489938.889 1227458.333
13509 490000.000 1222636.111

The first field is a 1-based index ID. The second and third fields represent coordinates derived from the latitude and longitude of U.S. cities with populations of 500 or greater. I created a new WPF application as described in the previous section, added a text-file item to the project and copied the city data to the file. I commented out the header lines of the data file by prepending double-slash (//) characters to those lines.

To create the scatter plot shown in Figure 8, I only needed to make minor changes to the example presented in the previous section. I modified the MapInfo class members as follows:

public int id;
  public double lat;
  public double lon;

Figure 9 shows the key processing loop in the revised LoadMapInfo method.

Figure 9 Loop for Scatter Plot

while ((line = sr.ReadLine()) != null)
{
  if (line.StartsWith("//"))
    continue;
  else {
    string[] pieces = line.Split(' ');
    int id = int.Parse(pieces[0]);
    double lat = double.Parse(pieces[1]);  
    double lon = -1.0 * double.Parse(pieces[2]);  
    MapInfo mi = new MapInfo(id, lat, lon);
    result.Add(mi);
  }
}

I had the code check to see if the current line begins with my program-defined comment tokens, and if so, skip over it. Notice that I multiplied the longitude-derived field by -1.0 because longitudes go from east to west (or right to left) along the x-axis. Without the -1.0 factor, my map would be a mirror image of the correct orientation.

When I populated my raw data arrays, all I had to do was ensure I correctly associated latitude and longitude to the y-axis and the x-axis, respectively:

for (int i = 0; i < mapInfoList.Count; ++i)
{
  ids[i] = mapInfoList[i].id;
  xs[i] = mapInfoList[i].lon;
  ys[i] = mapInfoList[i].lat;
}

If I had reversed the order of the associations, the resulting map would have been tilted on its edge. When I plotted my data, I needed only one small tweak to make a scatter plot instead of a line graph:

plotter.AddLineGraph(compositeDataSource,
  new Pen(Brushes.White, 0),
  new CirclePointMarker { Size = 2.0, Fill = Brushes.Red },
  new PenDescription("U.S. cities"));

By passing a 0 value to the Pen constructor, I specified a 0-width line, which effectively removed the line and created a scatter plot rather than a line graph. The resulting graph is pretty cool, and the program that generated the graph took only a few minutes to write. Believe me, I’ve tried many other approaches to plotting geographic data, and using WPF with the DynamicDataDisplay library is among the best solutions I’ve found.

Graphing Made Easy

The techniques I’ve presented here can be used to programmatically generate graphs. The key to the technique is the DynamicDataDisplay library from Microsoft Research. When used as a standalone technique to generate graphs in a software production environment, the approach is most useful if the underlying data changes frequently. When used in an application as an integrated technique to generate graphs, the approach is most useful with WPF or Silverlight applications. And as those two technologies evolve, I’m sure we’ll see more great visual-display libraries based on them.     


Dr. James McCaffrey works for Volt Information Sciences Inc. where he manages technical training for software engineers working at the Microsoft Redmond, Wash., campus. He has worked on several Microsoft products, including Internet Explorer and MSN Search. McCaffrey is the author of “.NET Test Automation Recipes: A Problem-Solution Approach” (Apress, 2006). He can be reached at jammc@microsoft.com.