Download the code sample
Generating graphics based on 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, and then use the Excel built-in drawing feature to manually generate graphics. This applies to most situations, but if the underlying data changes frequently, manually creating graphics can quickly become tedious. In this month's column, I will show you how to automate this process using Windows Presentation Foundation (WPF) technology. To see what I'm looking at, take a look at Figure 1. The graph shows the count of open and closed errors by date, and is dynamically generated using a short WPF program that reads data from a simple text file.
Figure 1 programmatically generated error count graph
An open error (indicated by a red circle on a blue line) rapidly increases shortly after the start of development, and then gradually decreases over time (this is information that may be useful when estimating the 0 error bounce date). Closed errors (triangle markers on green lines) are steadily increasing.
While this information may be useful, development resources are often limited in production environments, so it may not be worthwhile to manually generate such graphs. But with the technology I'll explain, it's quick and easy to create such graphs.
In the following sections, I'll show and explain in detail the C # code used to generate the graphics in Figure 1 . This column assumes that you have intermediate knowledge of C # Coding and a basic understanding of WPF. However, even if you have not contacted these two areas before, I think you can understand what I am talking about. I'm sure you'll find this technique to be an interesting and useful addition to your comprehensive skills.
Build a project
I start Visual Studio 2008 First and create a new C # project using the WPF application template. Select the. NET Framework 3.5 Library from the drop-down control in the upper-right area of the new Project dialog box. Name the project Buggraph. Although you can use WPF primitives to generate graphics programmatically, I use the handy Dynamicdatadisplay library (developed by the Microsoft Research Lab).
You can download the library from the CodePlex Open source hosting site in Codeplex.com/dynamicdatadisplay. I'll add a reference to the DLL in the project by saving the copy in the root of the Buggraph project, then right-clicking the project name, selecting the Add Reference option and pointing to the DLL file in the root directory.
Next, create the source data. In a production environment, your data can be in an Excel spreadsheet, a SQL database, or an XML file. For the sake of simplicity, I use a simple text file. In the Visual Studio Solution Explorer window, right-click the project name and choose Add | From the context menu New Item. Then select the text file item, rename the file to BugInfo.txt, and click the Add button. Here is the virtual 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 row contains a date, the second field contains the number of open errors associated with the date, and the third field displays the number of closed errors. As you'll see later, the Dynamicdatadisplay library can handle most types of data.
Next, I double-click the Window1.xaml file to load the project's UI definition. Add a reference to the drawing library DLL and slightly modify the default Width, Height, and Background attributes for the WPF display area as follows:
Copy
xmlns:d3= "http://research.microsoft.com/DynamicDataDisplay/1.0" title= "Window1" windowstate= "Normal" height= "500" Width= "background=" "Wheat" >
Then, add the key drawing objects, as shown in Figure 2 .
Figure 2 Adding a key drawing object
Copy
<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 primary display object. In the definition of this element, I added the declaration of the horizontal date axis and the vertical integer axis. The default axis type of the Dynamicdatadisplay library is a number with a fractional part (called a double type in C # terminology) that does not require an explicit axis declaration. I also added a header title declaration and an axis caption declaration. Figure 3 shows the design so far.
Figure 3 buggraph Program Design
Go to Source code
Once you have configured the static content for your project, you are ready to add code that reads the source data and generates the graphics programmatically. In the Solution Explorer window, double-click the Window1.xaml.cs file to load the C # file into the Code Editor. Figure 4 lists the complete source code for the program that generated the graphic in Figure 1 .
Figure 4 source code for the Buggraph project
Copy
Using system;using system.collections.generic;using system.windows;using System.Windows.Media; Penusing system.io;using Microsoft.Research.DynamicDataDisplay; Core functionalityusing Microsoft.Research.DynamicDataDisplay.DataSources; Enumerabledatasourceusing Microsoft.Research.DynamicDataDisplay.PointMarkers; Circlepointmarkernamespace buggraph{public partial class Window1:window {public Window1 () {Initialize Component (); 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 = Bru Shes. 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&L T 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 removed the unnecessary using namespace statements (such as System.Windows.Shapes) generated by the Visual Studio template. The using statement is then added to the three namespaces in the Dynamicdatadisplay library, eliminating the need to fully qualify their names. Next, add an event to the program-defined main routine in the Window1 constructor:
Copy
Loaded + = new Routedeventhandler (window1_loaded);
Here is the beginning of the main routine:
Copy
private void Window1_loaded (object sender, RoutedEventArgs e) { list<buginfo> buginfolist = Loadbuginfo (".. \\.. \\BugInfo.txt "); ...
I declared a generic list object Buginfolist and populated the virtual data in the file BugInfo.txt into the list using a program-defined helper method named Loadbuginfo. To organize my error message, I declared a small helper class Buginfo, as shown in Figure 5 .
Figure 5 Helper Class Buginfo
Copy
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; }}
For simplicity, I declare three data fields as public types, rather than private types that are combined with get and set properties. Because Buginfo is just data, I can use the C # structure without using classes. The Loadbuginfo method opens the BugInfo.txt file and iterates through the file, parses each field, instantiates the Buginfo object, and stores each Buginfo object in the results list, as shown in Figure 6 .
Figure 6 Loadbuginfo method
Copy
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;}
Instead of reading and processing every row in the file, I can use the File.ReadAllLines method to read all the rows in the data file into an array of strings. Note that in order to make the code short and clear, I omitted the usual error-checking steps, but you should perform this check in a production environment.
Next, I declare and assign a value to three arrays, as shown in Figure 7 .
Figure 7 building an array
Copy
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 using the Dynamicdatadisplay library, it is often convenient to organize the data into a one-dimensional array set. As an alternative to my programming (the data is read into a list object and then data is transferred to an array), I can read the data directly into the array.
Next, I'll convert the data array to a special Enumerabledatasource type:
Copy
var datesdatasource = new Enumerabledatasource<datetime> (dates);d atesdatasource.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 drawn must be in a uniform format. I just passed three data arrays to the generic Enumerabledatasource constructor. In addition, you must tell the library the axis (x -axis or y -axis) associated with each data source. The Setxmapping and Setymapping methods accept the method delegate as a parameter. Instead of defining an explicit delegate, I used a lambda expression to create an anonymous method. The basic axis data type of the Dynamicdatadisplay library is double. The setxmapping and Setymapping methods map my Special data type to a double type.
On the x -axis, I use the Converttodouble method to explicitly convert DateTime data to a double type. On the y -axis, I just write y = = y (read "Y to Y"), and the input int y is implicitly converted to the output double Y. I can also explicitly type-map by writing setymapping (y = convert.todouble (y). I can arbitrarily choose x and y as the parameters of the lambda expression, that is, I can use any parameter name.
The next step is to combine the x -axis and y -axis data sources:
Copy
Compositedatasource CompositeDataSource1 = new Compositedatasource (Datesdatasource, Numberopendatasource); Compositedatasource compositeDataSource2 = new Compositedatasource (Datesdatasource, Numbercloseddatasource); ...
The screen in Figure 1 shows the two data series plotted in the same drawing, that is, the number of errors opened and the number of errors that have been closed. Each composite data source defines a data series, so I need two separate data sources here: one for the number of open errors and one for the number of closed errors. When the data is all ready, you actually need only one statement to draw the data points:
Copy
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 Compositedatasource, which defines the error to draw and information about the exact drawing method. Here, I instruct the plotter object named plotter (defined in the Window1.xaml file) to do the following: Draw a graphic with a blue line of thickness of 2, place a circle marker with a red border and a red fill with a size of 10, and add a series title Number bugs open. It's so ingenious! As one of many alternative methods, I can use the
Copy
Plotter. Addlinegraph (CompositeDataSource1, colors.red, 1, "Number Open")
To draw a thin red line without a marker. Or, I can create dashed lines instead of solid lines:
Copy
Pen Dashedpen = new Pen (Brushes.magenta, 3);d Ashedpen.dashstyle = Dashstyles.dashdot;plotter. Addlinegraph (CompositeDataSource1, Dashedpen, new Pendescription ("Open bugs"));
My program will finally draw a second data series:
Copy
... 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 instruct the plotter to use green lines with triangular markers, which have a black border and a yellow-green fill. The Fittoview method scales the graph to the size of the WPF window.
After you instruct Visual Studio to build the Buggraph project, I get the BugGraph.exe executable file that can be started manually or programmatically at any time. I just need to edit the BugInfo.txt file to update the underlying data. Because the entire system is based on. NET Framework code, I can easily integrate drawing capabilities into any WPF project without having to deal with cross-technical issues. The Dynamicdatadisplay library also has a Silverlight version, so I can also add programming drawing capabilities to the Web application.
Scatter chart
The techniques shown in the previous section can be applied to all types of data, not just the data associated with the test. Let's take a quick look at another simple but impressive example. The screen in Figure 8 shows 13,509 American cities.
Fig . 8 example of a scatter chart
You may be able to identify locations in Florida, Texas, South California State and the Great Lakes. I got the data from this scatter plot from a library that was designed to be used in the travel quotient problem (www.iwr.uni-heidelberg.de/groups/comopt/software/TSPLIB95), This is one of the most famous and widely researched topics in the field of computer science. The file I used usa13509.tsp.gz similar to:
Copy
name:usa13509 (Other header information) 1 245552.778 817827.7782 247133.333 810905.5563 247205.556 810188.889...13507 48 9663.889 972433.33313508 489938.889 1227458.33313509 490000.000 1222636.111
The first field is an index ID that starts at 1. The second and third fields represent coordinates derived from the latitude and longitude of an American city with 500 or more people. I created a new WPF application as described in the previous section, added a text file entry to the project, and copied the city data into the file. I commented out these lines by adding a double slash (//) character before the header line of the data file.
To create a scatter plot as shown in Figure 8 , I just need to make a little change to the example shown in the previous section. I modified the MapInfo class member as follows:
Copy
public int id; public double lat; public double lon;
Figure 9 shows the key processing loops in the modified Loadmapinfo method.
Figure 9 The loop of a scatter chart
Copy
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 have the code check whether the current line starts with a program-defined comment tag, and if so, skips that line. Notice that I multiply the longitude-derived field by 1.0 because the longitude is from east to west (or right-to-left) in the x -axis direction. If you do not use the-1.0 factor, my map will be a mirrored image in the correct direction.
When I populate the original data array, just make sure that the latitude and longitude are associated with the y axis and the x axis, respectively:
Copy
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 reverse the association order, the resulting map will tilt along its edge. When I draw the data, I just need to tweak it a little bit to create a scatter chart instead of a line chart:
Copy
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 line with a width of 0, which effectively removes the line, creating a scatter plot instead of a line chart. The resulting graphic works great, and it takes only a few minutes to write the program that was born into the graph. Believe me, I've tried many other ways to draw geographic data, and using WPF and dynamicdatadisplay libraries together is one of the best solutions I've ever found.
Easy Drawing
The techniques I've shown here can be used to generate graphics programmatically. The key to this technology is the Dynamicdatadisplay library provided by the Microsoft Academy. If you are using a software production environment as an independent technology to generate graphics, this method is most useful when the underlying data changes frequently. This method is most useful for WPF or Silverlight applications if you are using integration techniques in your application to generate graphics. With the evolution of these two technologies, I'm sure I'll see more of the best visual display libraries based on both of these technologies.
Dr. James McCaffrey He worked for the Volt Information Sciences, Inc., where he was responsible for managing technical training for software engineers at the Microsoft headquarters campus in the state of Redmond, Washington. He has been involved in a number of Microsoft product development efforts, including Internet Explorer and MSN Search. McCaffrey is the author of the book ". NET Test Automation recipes:a problem-solution Approach" (apress,2006 year). You can contact him by [email protected] .
Using WPF to generate graphics