January 2010

Volume 25 Number 01

ExtremeUI - Line Charts with Data Templates

By Charles Petzold | January 2010

Download the Code Sample

Despite the many advanced manifestations of computer graphics these days (including animation and 3-D), I suspect that the most important will forever be the basic visual representation of data in traditional charts built from bars, pies and lines.

A table of data may appear like a jumble of random numbers, but any trends or interesting information hidden within the figures become much more comprehensible when displayed in a chart.

With the Windows Presentation Foundation (WPF)—and its Web-based offshoot, Silverlight—we have discovered the advantages of defining 
graphical visuals in markup rather than code. Extensible Application Markup Language (XAML) is easier to alter than code, easier to experiment with and is more toolable than code, allowing us to define our visuals interactively and to play around with alternative approaches.

In fact, defining visuals entirely in XAML is so advantageous that WPF programmers will spend many hours writing code specifically to enable more powerful and flexible XAML. This is a phenomenon I call “coding for XAML,” and it’s one of several ways that WPF has altered our approach to application development.

Many of the most powerful techniques in WPF involve the ItemsControl, which is the basic control to display collections of items, generally of the same type. (One familiar control that derives from ItemsControl is the ListBox, which allows navigation and selection as well as display.)

An ItemsControl can be filled with objects of any type—even business objects that have no intrinsic textual or visual representation. The magic ingredient is a DataTemplate—almost always defined in XAML—that gives those business objects a visual representation based on the objects’ properties.

In the March issue I showed how to use ItemsControls and DataTemplates to define bar charts and pie charts in XAML. Originally I was going to include line charts in that article, but the importance (and difficulty) of line charts mandates that a whole column be devoted to the subject.

Line Chart Issues

Line charts are actually forms of scatter plots—a Cartesian coordinate system with one variable on the horizontal axis and another on the vertical axis. One big difference with the line chart is that the values graphed horizontally are generally sorted. Very often these values are dates or times—that is, the line chart often shows the change in a variable over time.

The other big difference is that the individual data points are often connected with a line. Although this line is obviously a basic part of the line-chart visuals, it actually throws a big monkey wrench into the process of realizing the chart in XAML. The DataTemplate describes how each item in the ItemsControl is rendered; but connecting the items requires access to multiple points, ideally a PointCollection that can then be used with a Polyline element. The need to generate this PointCollection was the first hint that a custom class would be needed to perform pre-processing on the line-chart data.

More than other graphs, line graphs mandate that much more attention be paid to the axes. In fact, it makes sense for the horizontal and vertical axes themselves to be additional ItemsControls! Additional DataTemplates for these two other ItemsControls can then be used to define the formatting of the axis tick marks and labels entirely in XAML.

In summary, what you start out with is a collection of data items with two properties: one property corresponding to the horizontal axis and the other to the vertical axis. To realize a chart in XAML, you need to get certain items from this data. First, you need point objects for each data item (to render each data point). You also need a PointCollection of all data items (for the line connecting the points), and two additional collections containing sufficient information to render the horizontal and vertical axes in XAML, including data for the labels and offsets for positioning the labels and tick marks.

The calculation of these Point objects and offsets obviously requires some information: the width and height of the chart and the minimum and maximum values of the data graphed on the horizontal and vertical axes.

But that’s not quite enough. Suppose the minimum value for the vertical axis is 127 and the maximum value is 232. In that case, you might want the vertical axis to actually extend from 100 to 250 with tick marks every 25 units. Or for this particular graph, you might want to always include 0, so the vertical axis extends from 0 to 250. Or perhaps you want the maximum value to always be a multiple of 100, so it goes from 0 to 300. If the values range from -125 to 237, perhaps you want 0 to be centered, so the axis might range from -300 to 300.

There are potentially many different strategies for determining what values the axes display, which then govern the calculation of the Point values associated with each data item. These strategies might be so varied that it makes sense to offer a “plug-in” option to define additional axis strategies as required for a particular chart.

The First Attempt

Programming failures are sometimes just as instructive as programming successes. My first attempt to create a line-charting class accessible from XAML wasn’t exactly a complete failure, but it was certainly headed in that direction.

I knew that to generate the collection of Point objects I would obviously need access to the collection of items in the ItemsControl, as well as the ActualWidth and ActualHeight of the control. For these reasons it seemed logical to derive a class from ItemsControl that I called LineChartItemsControl.

LineChartItemsControl defined several new read/write properties: HorizontalAxisPropertyName and VerticalAxisPropertyName provided the names of the items’ properties that would be graphed. Four other new properties provided LineChartItemsControl with minimum and maximum values for the horizontal and vertical axes. (This was a very simple approach to handling the axes that I knew would have to be enhanced at a later time.)

The custom control also defined three read-only dependency properties for data binding in XAML: a property named Points of type PointCollection and two properties called HorizontalAxisInfo and VerticalAxisInfo for rendering the axes.

LineChartItemsControl overrode the OnItemsSourceChanged and OnItemsChanged methods to be informed whenever changes were occurring in the items collection, and it installed a handler for the SizeChanged event. It was then fairly straightforward to put together all the available information to calculate the three read-only dependency properties.

Actually using LineChartItemsControl in XAML, however, was a mess. The easy part was rendering the connected line. That was done with a Polyline element with its Points property bound to the Points property of LineChartItemsControl. But defining a DataTemplate that would position the individual data was very hard. The DataTemplate only has access to the properties of one particular data item. Through bindings, the DataTemplate can access the ItemsControl itself, but how do you get access to positioning information that corresponds to that particular data item?

My solution involved a RenderTransform set from a MultiBinding that contained both a RelativeSource binding and referenced a BindingConverter. It was so complex that the day after I had coded it, I couldn’t quite figure out how it worked!

The complexity of this solution was a clear indication that I needed a whole different approach.

The Line Chart Generator in Practice

The reconceived solution was a class I called LineChartGenerator because it generates all the raw materials necessary to define the visuals of a chart entirely in XAML. One collection goes in (the actual business objects) and four collections come out—one for the data points, one for drawing the connected line and two more for the horizontal and vertical axes. This allows you to construct a chart in XAML that contains multiple ItemsControls (generally arranged in a four-by-four Grid, or larger if you want to include titles and other labels), each with its own DataTemplate to display these collections.

Let’s see how this works in practice. (All downloadable source code is contained in a single Visual Studio project named LineChartsWithDataTemplates. This solution has one DLL project named LineChartLib and three demonstration programs.)

The PopulationLineChart project contains a structure named CensusDatum that defines two properties of type int named Year and Population. The CensusData class derives from ObservableCollection of type CensusDatum and fills up the collection with U. S. decennial census data from the years 1790 (when the population was 3,929,214) through 2000 (281,421,906). Figure 1 shows the resultant chart.

Figure 1 The PopulationLineChart Display

image: The PopulationLineChart Display

All the XAML for this chart is in the Window1.xaml file in the PopulationLineChart project. Figure 2 shows the Resources section of this file. LineChartGenerator has its own ItemsSource property; in this example it’s set to the CensusData object. It’s also necessary to set the Width and Height properties here. (I realize this isn’t an optimum place for these values, and not quite conducive to the preferred method of layout in WPF, but I couldn’t work out a better solution.) These values indicate the interior dimensions of the chart excluding the horizontal and vertical axes.

Figure 2 The Resources Section of PopulationLineChart

<Window.Resources>
    <src:CensusData x:Key="censusData" />

    <charts:LineChartGenerator 
            x:Key="generator"
            ItemsSource="{Binding Source={StaticResource censusData}}"
            Width="300"
            Height="200">

        <charts:LineChartGenerator.HorizontalAxis>
            <charts:AutoAxis PropertyName="Year" />
        </charts:LineChartGenerator.HorizontalAxis>

        <charts:LineChartGenerator.VerticalAxis>
            <charts:IncrementAxis PropertyName="Population"
                                  Increment="50000000"
                                  IsFlipped="True" />
        </charts:LineChartGenerator.VerticalAxis>
    </charts:LineChartGenerator>
</Window.Resources>

LineChartGenerator also has two properties of type AxisStrategy named HorizontalAxis and VerticalAxis. AxisStrategy is an abstract class that defines several properties, including PropertyName where you indicate the property of the data objects you want graphed on this axis. In accordance with WPF’s coordinate system, increasing values go from left to right and from top to bottom. Almost always you’ll want to set the IsFlipped property on the vertical axis to True so increasing values go from bottom to top.

One of the classes that derives from AxisStrategy is IncrementAxis, which defines one property named Increment. With the IncrementAxis strategy, you specify what increment you want between the tick marks. The minimum and maximum are set as multiples of the increment. I’ve used IncrementAxis for the population scale.

Another class that derives from AxisStrategy is AutoAxis, which defines no additional properties of its own. I’ve used this one for the horizontal axis: All it does is use the actual values for the axis. (Another obvious AxisStrategy derivative that I haven’t written is ExplicitAxis, where you supply a list of values to appear on the axis.)

The LineChartGenerator class defines two read-only dependency properties. The first is named Points of type PointCollection; use this property to draw the line that connects the points:

<Polyline Points="{Binding Source={StaticResource generator}, 
                           Path=Points}"
          Stroke="Blue" />

The second LineChartGenerator property is named ItemPoints of type ItemPointCollection. An ItemPoint has two properties, named Item and Point. Item is the original object in the collection—in this particular example, Item is an object of type CensusDatum. Point is the point where that item is to appear in the graph.

Figure 3 shows the ItemsControl that displays the main body of the chart. Notice that its ItemsSource is bound to the ItemPoints property of the LineChartGenerator. The ItemsPanel template is a Grid, and the ItemTemplate is a Path with an EllipseGeometry and a ToolTip. The Center property of the EllipseGeometry is bound to the Point property of the ItemPoint object, while the ToolTip accesses the Year and Population properties of the Item property.

Figure 3 The Main ItemsControl for PopulationLineChart

<ItemsControl ItemsSource="{Binding Source={StaticResource generator}, 
                                    Path=ItemPoints}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid IsItemsHost="True" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Path Fill="Red" RenderTransform="2 0 0 2 0 0">
                <Path.Data>
                    <EllipseGeometry Center="{Binding Point}"
                                     RadiusX="4"
                                     RadiusY="4"
                                     Transform="0.5 0 0 0.5 0 0" />
                </Path.Data>
                <Path.ToolTip>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Item.Year}" />
                        <TextBlock Text="{Binding Item.Population, 
                            StringFormat=’: {0:N0}’}" />
                    </StackPanel>
                </Path.ToolTip>
            </Path>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

You might be wondering about the Transform set on the EllipseGeometry object, which is offset by a RenderTransform property set on the Path element. This is a kludge: Without it, the ellipse at the far right was partially clipped, and I couldn’t fix it with ClipToBounds.

The Polyline and this main ItemsControl share the same single-cell Grid whose Width and Height are bound to the values from LineChartGenerator:

<Grid Width="{Binding Source=
{StaticResource generator}, Path=Width}"
      Height="{Binding Source=
{StaticResource generator}, Path=Height}">

The Polyline is underneath the ItemsControl in this example.

The AxisStrategy class defines its own read-only dependency property named AxisItems, a collection of objects of type AxisItem, which has two properties named Item and Offset. This is the collection used for the ItemsControl for each axis. Although the Item property is defined to be of type object, it will actually be the same type as the property associated with that axis. Offset is a distance from the top or left.

Figure 4 shows the ItemsControl for the horizontal axis; the vertical axis is similar. The ItemsSource property of the ItemsControl is bound to the AxisItems property of the HorizontalAxis property of the LineChartGenerator. The ItemsControl is thus filled with objects of type AxisItem. The Text property of the TextBlock is bound to the Items property, and the Offset property is used to translate the tick mark and text along the axis.

Figure 4 Markup for the Horizontal Axis of PopulationLineChart

<ItemsControl Grid.Row="2"
              Grid.Column="1"
              Margin="4 0"
              ItemsSource="{Binding Source={StaticResource generator}, 
                                    Path=HorizontalAxis.AxisItems}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid IsItemsHost="True" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <Line Y2="10" Stroke="Black" />
                <TextBlock Text="{Binding Item}"
                           FontSize="8"
                           LayoutTransform="0 -1 1 0 0 0"
                           RenderTransform="1 0 0 1 -6 1"/>

                <StackPanel.RenderTransform>
                    <TranslateTransform X="{Binding Offset}" />
                </StackPanel.RenderTransform>
            </StackPanel>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Because these three ItemsControls are simply sitting in three cells of a Grid, it’s the responsibility of the person designing the layout in XAML to make sure they align correctly. Any borders or margins or padding applied to these controls must be consistent. The ItemsControl in Figure 4 has a horizontal margin of 4; the ItemsControl for the vertical axis has a vertical margin of 4. I chose these values to correspond to the BorderThickness and Padding of a Border surrounding the single-cell Grid that contains the Polyline and the chart itself:

<Border Grid.Row="1"
        Grid.Column="1" 
        Background="Yellow"
        BorderBrush="Black"
        BorderThickness="1"
        Padding="3">

Data Type Consistency

The LineChartGenerator class itself is not very interesting. It assumes that the ItemsSource collection is already sorted, and is mostly devoted to making sure that everything gets updated when the ItemsSource property changes. If the collection set to ItemsSource implements ICollectionChanged, the chart is also updated when items are added to or removed from the collection. If the items in the collection implement INotifyPropertyChanged, the chart is updated when the items themselves change.

Most of the real work actually goes on in AxisStrategy and 
derived classes. These AxisStrategy derivatives are the classes you set to LineChartGenerator’s HorizontalAxis and VerticalAxis properties.

AxisStrategy itself defines the important PropertyName property that indicates which property of the objects being charted is associated with that axis. AxisStrategy uses reflection to access that particular property of the objects in the collection. But just accessing that property isn’t enough. AxisStrategy (and its derivatives) need to perform calculations on the values of this property to obtain Point objects and tick offsets. These calculations include multiplication and division.

The necessity of calculations strongly implies that the properties being graphed must be numeric types—integers or floating point. Yet one extremely common data type commonly used on the horizontal axis of line charts is not a number at all, but a date or a time. In the Microsoft .NET Framework, we’re talking about an object of type DateTime.

What do all the numeric data types and DateTime have in common? They all implement the IConvertible interface, which means that they all contain a bunch of methods that convert them into one another, and they are all usable with the same-named methods in the static Convert class. Therefore, it seemed reasonable to me to require that the properties being charted implement IConvertible. AxisStrategy (and its derivatives) could then simply convert the property values to doubles to perform the necessary calculations.

However, I soon discovered that properties of type DateTime actually cannot be converted to doubles using either the 
ToDouble method or the Convert.ToDouble static method. This meant that properties of type DateTime really had to be handled with special logic, which fortunately didn’t turn out to be a big deal. The Ticks property defined by DateTime is a 64-bit integer, which can be converted to a double; a double can be converted back to a DateTime by first converting it to a 64-bit integer and then passing that value to a DateTime constructor. A little experimentation revealed that the round-trip conversion was accurate to the millisecond.

AxisStrategy has a Recalculate method that loops through all the items in its parent’s ItemsSource collection, converts the specified property of each object to a double and determines the minimum and maximum values. AxisStrategy defines three properties that potentially affect these two values: Margin (which allows the minimum and maximum to be a little beyond the range of actual values), IncludeZero (so that the axis always includes the value zero even if all the values are greater than zero or less than zero), and IsSymmetricAroundZero, which means that the axis maximum should be positive, and the minimum should be negative, but they should have the same absolute values.

After those adjustments, AxisStrategy calls the abstract CalculateAxisItems method:

protected abstract void CalculateAxisItems(Type propertyType, ref double minValue, ref double maxValue);

The first argument is the type of the properties corresponding to that axis. Any class that derives from AxisStrategy must implement this method and use the opportunity to define the items and offsets that constitute its AxisItems collection.

It’s very likely that CalculateAxisItems will also set new minimum and maximum values. When CalculateAxisItems returns, AxisStrategy then uses these values together with the width and height of the chart to calculate Point values for all the items.

XML Data Sources

Besides dealing with numeric properties and properties of type DateTime, AxisStategy also must handle the case when the items in the ItemsSource collection are of type XmlNode. This is what the collection contains when ItemsSource is bound to an XmlDataProvider, either referencing an external XML file or an XML data island within the XAML file.

AxisStrategy uses the same conventions as DataTemplates: A name by itself refers to an XML element, and a name preceded by an @ sign is an XML attribute. AxisStrategy obtains these values as strings. Just in case they are actually dates or times, AxisStrategy first attempts to convert these strings into DateTime objects before converting them into doubles. DateTime.TryParseExact is used for this job, and only for the invariant-culture formatting specifications of “R”, “s”, “u” and “o”.

The SalesByMonth project demonstrates graphing XML data and a few other features. The Window1.xaml file contains an XmlDataProvider with data for 12 months of fictitious sales of two products named Widgets and Doodads:

<XmlDataProvider x:Key="sales"
                 XPath="YearSales">
    <x:XData>
        <YearSales >
            <MonthSales Date="2009-01-01T00:00:00">
                <Widgets>13</Widgets>
                <Doodads>285</Doodads>
            </MonthSales>

        ...

            <MonthSales Date="2009-12-01T00:00:00">
                <Widgets>29</Widgets>
                <Doodads>160</Doodads>
            </MonthSales>
        </YearSales>
    </x:XData>
</XmlDataProvider>

The Resources section also contains two very similar LineChartGenerator objects for the two products. Here’s the one for the Widgets:

<charts:LineChartGenerator 
               x:Key="widgetsGenerator"
               ItemsSource=
               "{Binding Source={StaticResource sales}, 
                                     XPath=MonthSales}"
               Width="250" Height="150">
    <charts:LineChartGenerator.HorizontalAxis>
        <charts:AutoAxis PropertyName="@Date" />
    </charts:LineChartGenerator.HorizontalAxis>
    
    <charts:LineChartGenerator.VerticalAxis>
        <charts:AdaptableIncrementAxis 
        PropertyName="Widgets"
        IncludeZero="True"
        IsFlipped="True" />
    </charts:LineChartGenerator.VerticalAxis>
</charts:LineChartGenerator>

Notice that the horizontal axis is associated with the XML attribute of Date. The vertical axis is of type AdaptableIncrementAxis, which derives from AxisStrategy and defines two additional properties:

•       Increments of type DoubleCollection

•       MaximumItems of type int

The Increments collection has default values 1, 2 and 5, and the MaximumItems property has a default value of 10. The SalesByMonth project simply uses those defaults. AdaptableIncrementAxis determines the optimum increment between tick marks so the number of axis items does not exceed MaximumItems. With the default settings, it tests increment values of 1, 2 and 5, and then 10, 20 and 50, and then 100, 200 and 500 and so forth. It will also go in the opposite direction: testing increments of 0.5, 0.2, 0.1 and so forth.

You can fill the Increments property of AdaptableIncrementAxis with other values, of course. If you want the increment to always be a multiple of 10, just use the single value 1. An alternative to 1, 2 and 5 that might be more appropriate for some situations is 1, 2.5 and 5.

AdaptableIncrementAxis (or something like it of your own invention) is probably the best choice when the numeric values of an axis are unpredictable, particularly when the chart contains data that is dynamically changing or growing in overall size. Because the Increments property of AdaptableIncrementAxis is of type DoubleCollection, it’s unsuitable for DateTime values. I describe an alternative for DateTime later in this column.

The XAML file in the SalesByMonth project defines two LineChartGenerator objects for the two products, which then allows a composite chart as shown in Figure 5.

Figure 5 The SalesByMonth Display

image: The SalesByMonth Display

This option of creating a composite chart did not require anything special in the classes that make up LineChartLib. All the code does is generate collections that can then be handled flexibly in XAML.

To accommodate all the labels and axes, the entire chart is realized in a Grid of four rows and five columns containing five ItemsControls—two for the two collections of data items in the chart itself, two for the axis scales on the left and right, and one more for the horizontal axis.

The color-coding to distinguish the two products is simple to implement in XAML. But notice also that the two products are further distinguished by triangular and square data points. The triangular items are rendered by this DataTemplate:

<DataTemplate>
    <Path Fill="Blue"
          Data="M 0 -4 L 4 4 -4 4Z">
        <Path.RenderTransform>
            <TranslateTransform X="{Binding Point.X}"
                                Y="{Binding Point.Y}" />
        </Path.RenderTransform>
    </Path>
</DataTemplate>

In a real-life example you might use shapes actually associated with the two products, or even little bitmaps.

The line that connects the points in this example is not a standard Polyline element but instead a custom Shape derivative named CanonicalSpline. (The canonical spline—also known as the cardinal spline—is part of Windows Forms but did not make it into the WPF. Every pair of points is connected by a curve that depends algorithmically on the two additional points surrounding the pair of points.) It’s also possible to write other custom classes for this purpose, perhaps one that performs least-squares interpolation on the points and displays the result.

The HorizontalAxis.AxisItems property of the LineChartChartGenerator is an ObservableCollection of type DateTime, which means that the items can be formatted using the StringFormat feature of the Binding class and standard date/time formatting strings.

The DataTemplate for the horizontal axis uses the “MMMM” formatting string to display whole month names:

<DataTemplate>
    <StackPanel HorizontalAlignment="Left">
        <Line Y2="10" Stroke="Black" />
        <TextBlock Text="{Binding Item, StringFormat=MMMM}"
                   RenderTransform="1 0 0 1 -4 -4">
            <TextBlock.LayoutTransform>
                <RotateTransform Angle="45" />        
            </TextBlock.LayoutTransform>
        </TextBlock>
        
        <StackPanel.RenderTransform>
            <TranslateTransform X="{Binding Offset}" />
        </StackPanel.RenderTransform>
    </StackPanel>
</DataTemplate>

Dates and Times

The use of DateTime objects on the horizontal axis of a line chart is so common that it’s worth spending effort coding an AxisStrategy to deal specifically with these objects. Some line charts accumulate data such as stock prices or environmental readings, perhaps adding a new item every hour or so, and it would be nice to have an AxisStrategy that adapts itself depending on the range of DateTime values among the graphed items.

My stab at such a class is calledAdaptableDateTimeAxis, and it’s intended to accommodate DateTime data over a wide range from seconds to years.

AdaptableDateTimeAxis has a MaximumItems property (with a default setting of 10) and six collections called SecondIncrements, MinuteIncrements, HourIncrements, DayIncrements, MonthIncrements and YearIncrements. The class systematically tries to find an increment between tick points so that the number of items does not exceed MaximumItems. With the default settings, AdaptableDateTimeAxis will test increments of 1 second, 2 seconds, 5, 15 and 30 seconds, then 1, 2, 5, 15 and 30 minutes, then 1, 2, 4, 6 and 12 hours, 1, 2, 5 and 10 days, and 1, 2, 4 and 6 months. Once it gets up to years, it tries 1, 2 and 5 years, then 10, 20 and 50, and so forth.

AdaptableDateTimeAxis also defines a read-only dependency property named DateTimeInterval—also the name of an enumeration with members Second, Minute, Hour and so forth—that indicates the units of the axis increments determined by the class. This property allows DataTriggers to be defined in XAML that alter the DateTime formatting based on the increment. Figure 6 shows a sample DataTemplate that performs such formatting selection.

Figure 6 The DataTemplate for the Horizontal Axis of TemperatureHistory

<DataTemplate>
    <StackPanel HorizontalAlignment="Left">
        <Line Y2="10" Stroke="Black" />

        <TextBlock Name="txtblk"
                   RenderTransform="1 0 0 1 -4 -4">
            <TextBlock.LayoutTransform>
                <RotateTransform Angle="45" />        
            </TextBlock.LayoutTransform>
        </TextBlock>

        <StackPanel.RenderTransform>
            <TranslateTransform X="{Binding Offset}" />
        </StackPanel.RenderTransform>
    </StackPanel>

    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding Source={StaticResource generator}, 
                                       Path=HorizontalAxis.
                                       DateTimeInterval}" 
                     Value="Second">
            <Setter TargetName="txtblk" Property="Text" 
                 Value="{Binding Item, StringFormat=h:mm:ss d MMM yy}" />
        </DataTrigger>
        <DataTrigger Binding="{Binding Source={StaticResource generator}, 
                                       Path=HorizontalAxis.
                                       DateTimeInterval}" 
                     Value="Minute">
            <Setter TargetName="txtblk" Property="Text" 
                    Value="{Binding Item, StringFormat=h:mm d MMM yy}" />
        </DataTrigger>
        <DataTrigger Binding="{Binding Source={StaticResource generator}, 
                                       Path=HorizontalAxis.
                                       DateTimeInterval}" 
                     Value="Hour">
            <Setter TargetName="txtblk" Property="Text" 
                 Value="{Binding Item, StringFormat=’h tt, d MMM yy’}" />
        </DataTrigger>
        <DataTrigger Binding="{Binding Source={StaticResource generator}, 
                                       Path=HorizontalAxis.
                                       DateTimeInterval}" 
                     Value="Day">
            <Setter TargetName="txtblk" Property="Text" 
                    Value="{Binding Item, StringFormat=d}" />
        </DataTrigger>
        <DataTrigger Binding="{Binding Source={StaticResource generator}, 
                                       Path=HorizontalAxis.
                                       DateTimeInterval}" 
                     Value="Month">
            <Setter TargetName="txtblk" Property="Text" 
                    Value="{Binding Item, StringFormat=MMM yy}" />
        </DataTrigger>
        <DataTrigger Binding="{Binding Source={StaticResource generator}, 
                                       Path=HorizontalAxis.
                                       DateTimeInterval}" 
                     Value="Year">
            <Setter TargetName="txtblk" Property="Text" 
                    Value="{Binding Item, StringFormat=MMMM}" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

That template is from the TemperatureHistory project, which accesses the Web site of the National Weather Service to obtain hourly temperature readings in Central Park in New York City. Figure 7 shows the TemperatureHistory display after the program ran for several hours; Figure 8 shows it after several days.

Figure 7 The TemperatureHistory Display with Hours

image: The TemperatureHistory Display with Hours

Figure 8 The TemperatureHistory Display with Days

image: The TemperatureHistory Display with Days

Of course, my line-charting classes aren’t entirely flexible—for example, currently there is no way to independently draw tick marks not associated with text labels—but I think they illustrate a viable and powerful approach to providing sufficient information to define line-chart visuals entirely in XAML.


Charles Petzold is a longtime contributing editor to MSDN Magazine*. His most recent book is* The Annotated Turing: A Guided Tour through Alan Turing’s Historic Paper on Computability and the Turing Machine (Wiley, 2008). His Web site is charlespetzold.com.

Thanks to the following technical expert for reviewing this article: David Teitelbaum