Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

OpenWPFChart: assembling charts from components: Part I - Parts

0.00/5 (No votes)
19 Mar 2009 1  
Provides the component model along with base components to assemble charts.

Chart Samples

Abstract

The OpenWPFChart library is an open source project at CodePlex. Its goal is to provide a component model along with base components (parts) to make it possible to assemble different chart controls from these parts. The parts set is extensible so the developer can add his own new components. Chart controls composed from these parts could have absolutely different look and feel, as you can see in the figures above.

Rationale

Chart controls design is a rather complex task. Companies and individuals have proposed a wide spectrum of chart controls, both open source and proprietary. The problem is that it’s practically unreal to design a chart control or a control suite satisfying all user needs (and even foresee all these needs). At the first place, this is caused by differences in:

  • Control visual look.
  • Control elements set – function graphs, scattered point clouds, colored areas between curves, diagrams, coordinate grids and axes, legends, labels, sorts of markers, etc.
  • The way these elements are positioned at or around the chart area; e.g., coordinate axes; legends and labels might be placed either inside the chart area or along its borders or somewhere else.

The problem becomes even worst if a xhart control should provide editing capabilities of the data displayed. As far as I know, there is no chart control which is customizable enough to suit special developer needs. As a result, we see more and more new chart controls written from scratch.

Why not provide an open architecture of an extensible set of base elements to enable a developer to assemble different chart controls to fit exactly her/his needs and preferences (especially because WPF provides us with its wonderful composition capabilities)?

Solution

Usually, a chart behave as a collection of visual elements which can support or not support the idiom of “selected element(s)”. So, basically, a chart control may derive either from ItemsControl or from one of its derivatives: Selector, ListBox etc. Since data presentation visual chart items should share a common chart area screenspace, we could use a Canvas (or the like) as the ItemsControl.ItemsPanel to position the visuals in an ItemsControl. Further, in the ItemsControl's template, we could place the required decorative or functional elements like axes, grids, legends, labels, etc.

We could develop a set of elements (Visuals, FrameworkElements):

  • Data presentation visual elements: function curves, scattered point clouds, etc.
  • Coordinate grids.
  • Coordinate axes.

With these elements at hand, we could build a chart composed of them as follows:

  1. Data presentation visual elements (chart items) are displayed as ItemsControl items. We place the grids into an ItemsControl's template. If required, we could also place here the coordinate axes (if they should be alongside the chart items). The ItemsControl forms "The Chart Area".
  2. The chart area outskirts are decorated with coordinate axes, legends, labels, etc.

To manage all the things above, we aren’t even required to create the chart control; we could compose all the parts directly in the application window. Note, however, that the window XAML in such a case becomes rather unwieldy, so this approach isn’t practical; it’s better to create one or more chart controls (either Custom Controls or User Controls) with the required look and feel.

Parts

One part of OpenWPFChart is the object model of chart components (parts) along with the basic set of these elements.

Chart Items

A chart item is a composition of data and visuals to display the data in the chart area. It’s designed like:

Chart Item model

Figure 1. Chart Item model

First, there is a data object (Data) we want to see in the chart.

The Data object can be displayed in a number of ways: e.g., the points sequence could be displayed as a curve, as a bar graph, or as just the points cloud; and the same data object could be displayed more than once at the same chart or at another chart in the application.

DataView objects are here to make it all possible. In essence, the DataView is a wrapper of the Data. The DataView contains some additional information pertaining to the data presentation:

  • Information on the presentation state. The most important among others are coordinate scales, horizontal and vertical (see more on this later).
  • Drawing tools like brushes, pens, geometries, etc.

Data relates to DataView as one-to-many.

The DataView object doesn’t contain any rendering code. To push it to the View, we need a Visual Element (DataVisualElement) which can be a part of the WPF Visual Tree.

For example, let’s say we have a DataView containing a set of data points, the pen to draw a line between the points, and the geometry and the brush to draw the point marker. To render this DataView, we could have some DataVisualElements: one to draw it as a stepped line, another to draw it as a polyline, yet another to draw it as a some smooth curve, etc. So, the DataView relates to the DataVisualElements as one-to-many.

The DataView - DataVisualElement association is established through the WPF DataTemplate mechanism. There is one trick, however. Because a DataView type can be templated with different DataVisualElements, it should be a cue to make a choice. the DataView contains such a property of type object, which is usually set to the desired DataVisualElement type (although any other approach is admissible). Then, to associate the DataView to that DataVisualElement, a DataTemplateSelector is used.

The DataVisualElement should participate in the WPF rendering and layout so it inherits from FrameworkElement. Note, however, in the example above, three DataVisualElements draw a curve each on its own manner, but points drawing is the same in all three cases. So, it’s convenient to separate the curve and points drawing in different visuals. This allows to avoid code duplication and, what’s more important, separates different visuals in the Visual Tree, making their Hit Testing straightforward. That’s why, as shown above, DataVisualElements usually own one or more Visuals (DataVisual) which do the real rendering.

Note: I’m aware of the approach used by world-famous companies such as Infragistics and ComponentOne, which derive Chart Visual Elements not from FrameworkElement but from FrameworkContentElement: FrameworkContentElement is more lightweight than FrameworkElement. I don’t think this approach will provide some real gain in OpenWPFChart: only chart items inherit from FrameworkElement here, not a single point or a curve segment. But, a chart can’t contain more than ten chart items or so to avoid visual space clutter. So, why bother?

Coordinate Scales

A Coordinate Scale defines an interval of coordinate axis along with its scale – the relation between Coordinate Scales extent in WPF pixels and in data units. Each DataView object has two Coordinate Scales – abscissa (X) and ordinate (Y).

OpenWPFChart provides some Coordinate Scale classes, all derived from the abstract base class OpenWPFChart.ChartScale, which defines three properties: Start, Stop, and Scale. Each concrete ChartScale-derived class describes the coordinate axis in terms of that axis base data type: e.g., double, DateTime, or object. This base data type corresponds to the data type of the data, wrapped by the DataView object at the X and Y axes.

There could exist different ChartScale classes with the same base data type. For example, the ChartLinearScale and the ChartLogarithmicScale both have double base type, but have different coordinate axis interval restrictions: the ChartLogarithmicScale interval must fit to a positive double demi-axis.

The Scale property of different ChartScale classes has different meanings. E.g., for the ChartLinearScale, the Scale value is the count of WPF pixels per one data unit, whereas for ChartLogarithmicScale, it is the count of WPF pixels per Log10 base.

Besides defining an interval, each ChartScale class has properties describing the chart scale markup: the sequence of scale ticks used by the Coordinate Axes and the Grids elements. This feature provides for seamless integration between the Coordinate Axes and the Grids from one hand and DataView objects from the other hand by means of binding.

Coordinate Axes and Grids

Axis elements have a rich visual representation. Although it’s possible to use a single axis element to display any axis, in practice, some ChartScale types require specific axis element types. For example, logarithmic axis labels are usually displayed as 10 with a power in a superscript font. If desired, the developer can supply his own axis classes with specific look and feel.

In the chart composition, an axis is linked to the ChartScale data object it displays through the WPF DataTemplate:

Axis Model

Figure 2. Coordinate Axis Model

Thanks to the ChartScale class family design, a single OpenWPFChart Grid element is quite enough to display any coordinate grid in the chart area. It’s doubtful that someone would want to devise any other Grid element, although it’s possible.

Composition

After the developer has selected the OpenWPFChart predefined parts or has written extension parts she or he wants to see in the chart, we can proceed to the chart composition.

A chart could be composed from chart parts as either a reusable piece of code like a Custom or User Control, or directly at the WPF Window or Page. The latter approach has some drawbacks because it forces to place a lot of XAML code at the Windows or Page XAML but, except for the lack of maintainability and reusability, there is nothing wrong in this approach. Anyhow, the composition principles remain the same.

The steps the developer has to take are as follows:

  1. Select the chart area control.
  2. Define DataTemplates to link ItemDataView objects to chart item elements.
  3. Define DataTemplates to link ChartScale objects to axes elements.
  4. Define the step ItemsControl (chart area control) style.
  5. Optionally, define the control and the corresponding DataTemplates and a style for the chart legend element.

Chart Area

Chart area is a rectangular partition of the chart where chart items and grid lines are drawn. The chart area control is the one managing the chart items. It holds chart items as well as decorative elements like coordinate grids, axes (if they should be rendered at the chart area), text labels, etc.

Most naturally, the chart area control is an ItemsControl derived class. It could be the ItemsControl class itself if the new chart doesn’t require to support the selection. Or, it could be the Selector class or, more conveniently, the ListBox.

Chart Item Element DataTemplates

As always with WPF data presentation, classes and visuals are decoupled. The link between the two is established by means of the DataTemplate. By design, each chart item in the chart can have its own specific set of visuals. We can associate the data presentation objects with visuals either (1) on the type or (2) on the object basis.

In the first case, we could define the template like:

<DataTemplate DataType="{x:Type parts:SampledCurveDataView">
    <parts:PolylineSampledCurve ItemDataView="{Binding}"/>
</DataTemplate>

That means, we want to display a set of data points (SampledCurveDataView) as a polyline (along with the point visuals themselves). But, there is a drawback here: what if we want to display the same or another object of the SampledCurveDataView type as, say, a Bezier spline (i.e., with the BezierSampledCurve visual element)? How to associate the same SampledCurveDataView type with other visuals, and who will take the decision when to display one visual or another?

There are some ways to solve this problem, but the most appropriate one in this case is to go by the way of the second case: establish associations on the object basis. To do that, we should have some indication in the data presentation object as to which template to use with them. For that purpose, the ItemDataView base class defines the VisualCue property (of the object type). With that property, we can use the WPF DataTemplateSelector to select the template appropriate for the VisualCue property value. We should now use named DataTemplates instead of types:

<DataTemplate x:Key="polylineTemplate">
    <parts:PolylineSampledCurve ItemDataView="{Binding}"/>
</DataTemplate>
<DataTemplate x:Key="bezierTemplate">
    <parts:BezierSampledCurve ItemDataView="{Binding}"/>
</DataTemplate>
<DataTemplate x:Key="splineTemplate">
    <parts:SplineSampledCurve ItemDataView="{Binding}"/>
</DataTemplate>
<DataTemplate x:Key="scatteredPointsTemplate">
    <parts:ScatteredPoints ItemDataView="{Binding}"/>
</DataTemplate>

By its nature, the DataTemplateSelector must be implemented in code. But good design requires all the composition be accomplished in XAML. By chance, there are some means to implement a generic DataTemplateSelector class which can serve all our composition scenarios because it’s XAML-configurable. OpenWPFChart uses the modified version of the GenericDataTemplateSelector proposed by Nick Zhebrun (see Nick Zhebrun GenericDataTemplateSelector).

The OpenWPFChart.Parts.GenericDataTemplateSelector allows to specify associations between data presentation objects and visuals, like this:

<parts:GenericDataTemplateSelector x:Key="chartItemsTemplateSelector">
    <parts:GenericDataTemplateSelectorItem
        PropertyName="VisualCue" 
        Value="{x:Type parts:PolylineSampledCurve}"
        Template="{StaticResource polylineTemplate}"
        TemplatedType="{x:Type parts:SampledCurveDataView}"
        Description="Polyline Sampled Curve"/>
    <parts:GenericDataTemplateSelectorItem
        PropertyName="VisualCue" 
        Value="{x:Type parts:BezierSampledCurve}"
        Template="{StaticResource bezierTemplate}"
        TemplatedType="{x:Type parts:SampledCurveDataView}"
        Description="Bezier Sampled Curve"/>
    <parts:GenericDataTemplateSelectorItem
        PropertyName="VisualCue" 
        Value="{x:Type parts:SplineSampledCurve}"
        Template="{StaticResource splineTemplate}"
        TemplatedType="{x:Type parts:SampledCurveDataView}"
        Description="Spline Sampled Curve"/>
    <parts:GenericDataTemplateSelectorItem
        PropertyName="VisualCue" 
        Value="{x:Type parts:ScatteredPoints}"
        Template="{StaticResource scatteredPointsTemplate}"
        TemplatedType="{x:Type parts:ScatteredPointsDataView}"
        Description="Scattered points cloud"/>
</parts:GenericDataTemplateSelector>

This code snippet means that we associate an object of TemplatedType type, with a property named PropertyName and a value equals to Value, with the template Template.

This GenericDataTemplateSelector resource is then referenced in the chart area control definition, somewhat like this:

<ListBox 
    ItemsSource="{Binding}" 
    ItemTemplateSelector="{StaticResource chartItemsTemplateSelector}"
    ... />

Axes DataTemplates

In the OpenWPFChart library, a coordinate axis can be seen as the visual representation of the ChartScale derived type. The Axis element type can depend on the concrete ChartScale type and on the ChartScale base type (e.g., numeric, DateTime, etc.).

For example, the linear coordinate axis may be displayed with the LinearAxis, DateTimeAxis, or SeriesAxis elements depending on the bound ChartScale base type. Alternatively, any linear coordinate axis may be displayed with GenericLinearAxis. A numeric coordinate axis can be displayed in a linear or in a logarithmic fashion depending on whether it’s bound to the ChartLinearScale or ChartLogarithmicScale types.

It’s often important that the axis element type should change seamlessly if the bound ChartScale type changes at run time.

The Axis element AxisScale property should be bound to some source of the ChartScale derived type. It can be one of ItemDataView HorizontalScale or VerticalScale properties or a similar property of the chart control or the window constructed. To provide the ability, the Axis element type changes seamlessly with changes to the bound ChartScale type; the Axis element shouldn’t be bound directly to the sources mentioned above. Instead, it should be wrapped into some WPF ContentElement which binds to the ChartScale source. It could look like:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>

    <ContentPresenter Name="hAxisHost"
        Content="{Binding ElementName=mainWindow, Path=HorizontalScale}"/>
    <TextBlock Grid.Row="1" HorizontalAlignment="Center">
      Axis of abscissas
    </TextBlock>
</Grid>

for the horizontal axis, and like:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    
    <ContentPresenter Name="vAxisHost"
        Content="{Binding ElementName=mainWindow, Path=VerticalScale}"/>
    <TextBlock Grid.Row="1" HorizontalAlignment="Center">Axis of ordinates</TextBlock>
    
    <Grid.LayoutTransform>
        <RotateTransform Angle="90"/>
    </Grid.LayoutTransform>
</Grid>

for the vertical one.

In the code snippets above, it’s supposed that the inherited DataContext object contains the HorizontalScale and VerticalScale properties of the ChartScale type. Axes are defined alongside with axes labels, which are just TextBlocks in this sample. In the case of the vertical axis, its container Grid element is rotated so the axis becomes vertical.

The axis host wrapper element resolves its binding through one of the typed axis DataTemplates provided somewhere in its resource lookup scope. Below is an example of an axis DataTemplate:

<DataTemplate DataType="{x:Type parts:ChartLinearScale}">
    <parts:LinearAxis AxisScale="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type parts:ChartLogarithmicScale}">
    <parts:LogarithmicAxis AxisScale="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type parts:ChartDateTimeScale}">
    <parts:DateTimeAxis AxisScale="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type parts:ChartSeriesScale}">
    <parts:SeriesAxis AxisScale="{Binding}"/>
</DataTemplate>

With this design, the Axis element type follows the bound property ChartScale; the former changes automatically when the latter changes.

Chart Composition

We can compose the chart as a Custom Control, a User Control, or as a part of either a Window or a Page. But the principles remain the same, and there are some common steps to be accomplished.

First, we have to choose the chart area control. Most naturally, the chart area control is a ItemsControl derived class. It could be the ItemsControl class itself if the new chart doesn’t require to support selection. Or, it could be the Selector class or, more conveniently, the ListBox.

Second, we have to replace our chart area control ItemsPanel with a Canvas or another layout container, allowing our chart items to overlap. Somewhere in the control style, we should add a setter like this:

<Setter Property="ItemsPanel">
    <Setter.Value>
        <ItemsPanelTemplate>
            <Canvas />
        </ItemsPanelTemplate>
    </Setter.Value>
</Setter>

Third, we should set the control ItemTemplateSelector to the selector we defined somewhere in the resources (see Chart Item Element DataTemplates):

<Setter Property="ItemTemplateSelector" 
        Value="{StaticResource chartItemsTemplateSelector}"/>

The forth step is to define the chart area control ControlTemplate. It’s where we define our chart look and, so, it depends a lot on our intentions. However, there are some common issues worth a mention here.

At the chart area, we should define a placeholder for our chart items with a WPF ItemsPresenter and two coordinate Grids (vertical and horizontal) below the items.

Let’s imagine we develop a custom chart control derived from ListBox. The control style should contain the following code fragment:

<Setter Property="Template">
    <Setter.Value>
        <ControlTemplate TargetType="{x:Type local:CurveChart}">
            ……
            <!-- CurveChart area -->
            <Grid Grid.Column="1">
                <!-- Coordinate grids -->
                <parts:Grid Name="PART_VerticalGrid"
                    HorizontalScale="{TemplateBinding HorizontalScale}"
                    VerticalScale="{TemplateBinding VerticalScale}"
                    GridVisibility="{TemplateBinding VerticalGridVisibility}"
                />
                <parts:Grid Name="PART_HorizontalGrid" Orientation="Horizontal"
                    HorizontalScale="{TemplateBinding HorizontalScale}"
                    VerticalScale="{TemplateBinding VerticalScale}"
                    GridVisibility="{TemplateBinding HorizontalGridVisibility}"
                />
                <!-- CurveChart Items -->
                <ItemsPresenter Name="PART_ItemsHost"
                    SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
            </Grid>
            ……
        </ControlTemplate>
    </Setter.Value>
</Setter>

In the snippet above, it’s supposed the control has some properties (VerticalScale, VerticalGridVisibility, etc.). The coordinate grids are placed in the WPF Grid container, and the ItemsPresenter is placed at top of these grids so the chart items wouldn’t be cluttered with grid lines. Grid properties are bound to the control properties. ItemsPresenter will show its items by means of chart item DataTemplates (see Chart Item Element DataTemplates), which, in its turn, will bind the chart item properties to the control properties.

Using the Code

The code attached to this article is the Visual Studio 2008 SP1 solution targeted at .NET Framework 3.5.

It contains not just the OpenWPFChart parts discussed in this article, but also a set of samples on how to use these parts to compose charts either directly in a WPF Window or as a Custom Control. The input data sample files are supplied too.

Test projects as well as HTML Help have been stripped from the solution to decrease download size. To get the full code pack and documentation, go to OpenWPFChart at CodePlex.

Points of Interest

The methodology used in this article is an inherent part of the WPF composition model and is described in details in the excellent series of articles ItemsControl: A to Z published by Dr. WPF. It’s just applied here to the specific domain: Chart development.

See more

See OpenWPFChart control examples in the upcoming second article of this series.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here