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:
- 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".
- 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:
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 DataVisualElement
s: 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 DataVisualElement
s 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 DataVisualElement
s, 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 DataVisualElement
s 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, DataVisualElement
s 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
:
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:
- Select the chart area control.
- Define
DataTemplate
s to link ItemDataView
objects to chart item elements.
- Define
DataTemplate
s to link ChartScale
objects to axes elements.
- Define the step
ItemsControl
(chart area control) style.
- Optionally, define the control and the corresponding
DataTemplate
s 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 DataTemplate
s 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 TextBlock
s 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 DataTemplate
s 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 Grid
s (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}">
……
-->
<Grid Grid.Column="1">
-->
<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}"
/>
-->
<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 DataTemplate
s (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.