Introduction
I needed to use charts for the reports in an application I was working on. Having
used the WPF Toolkit controls like DataGrid and DatePicker for some time now,
I decided to use the included chart controls instead of using other third-party
controls. Currently, it is in preview quality as of the February 2010 release.
One problem that I encountered is that there is no built-in scrolling support.
Look at the following figure below. You can see that the axis labels overlap
and all the columns are fitted to the available area.

Figure 1. Overlapping Labels and Congested Points
I believe that there will be support for chart scrolling in the future. But for now
we have to devise a workaround. I have searched over the Internet for articles
that may have solved this problem but haven’t found any. In this example, I will
be working on a column chart.
Getting Started
Before we start coding, it is better if you have the WPF toolkit source code with
you as reference. This can be downloaded from this link. At the time of writing, the latest release was on February 2010.
Custom Axis
My first idea is just to add a Scrollbar control to the default template of the independent
axis and handle scroll events so I can show the coordinates and data points that
should be visible and hide those that are not. However, I decided to use a ScrollViewer
control because it seems to be easier to use. We only need to wrap the panel
containing the axis and update each data point’s position each time the scrollbar
is moved by the user. The following XAML shows the template of the axis, taken
from the default style of the DisplayAxis control.
<ControlTemplate TargetType="charting:DisplayAxis">
<Grid x:Name="AxisGrid" Background="{TemplateBinding Background}">
<datavis:Title x:Name="AxisTitle" Style="{TemplateBinding TitleStyle}" />
</Grid>
</ControlTemplate>
Listing 1. Default DisplayAxis Template
The Grid named AxisGrid will contain a Panel that will then be populated with coordinate
labels. In our example, the Panel will be filled up with categories, (Part0,
Part1, Part2, and on) since the horizontal axis is of type CategoryAxis. You
can see the different Axis classes in the next figure. If the Panel is accessible
in our code, we can wrap it inside a ScrollViewer. Another option is to put a
ScrollViewer around the parent Grid but that would mean the axis’ title will
be scrollable as well. In any case, we have to derive a class from the DisplayAxis
class or one of its descendants to implement scrolling.

Figure 2. Axis Classes
In our example, it might be natural to think that we need to subclass CategoryAxis.
However, I found little benefit from doing this. That is, we will only inherit
the SortOrder dependency property. Other fields are marked as private, even the
list of categories. As for the methods, we need to override their implementation
anyway. Thus, I decided to create a ScrollableCategoryAxis class which is basically
a copy of the CategoryAxis class, except the part where the Panel is populated
with the category labels. Here is the CategoryAxis’ implementation of adding
category labels to the Panel.
/// <summary>
/// Renders as an oriented axis.
/// </summary>
/// <param name="availableSize">The available size.</param>
private void RenderOriented(Size availableSize)
{
_labelPool.Reset();
_majorTickMarkPool.Reset();
try
{
OrientedPanel.Children.Clear();
this.GridLineCoordinatesToDisplay.Clear();
if (this.Categories.Count > 0)
{
double maximumLength = Math.Max(GetLength(availableSize) - 1, 0);
Action<double> placeTickMarkAt =
(pos) =>
{
Line tickMark = _majorTickMarkPool.Next();
OrientedPanel.SetCenterCoordinate(tickMark, pos);
OrientedPanel.SetPriority(tickMark, 0);
this.GridLineCoordinatesToDisplay.Add(new UnitValue(pos, Unit.Pixels));
OrientedPanel.Children.Add(tickMark);
};
int index = 0;
int priority = 0;
foreach (object category in Categories)
{
Control axisLabel = CreateAndPrepareAxisLabel(category);
double lower = ((index * maximumLength) / Categories.Count) + 0.5;
double upper = (((index + 1) * maximumLength) / Categories.Count) + 0.5;
placeTickMarkAt(lower);
OrientedPanel.SetCenterCoordinate(axisLabel, (lower + upper) / 2);
OrientedPanel.SetPriority(axisLabel, priority + 1);
OrientedPanel.Children.Add(axisLabel);
index++;
priority = (priority + 1) % 2;
}
placeTickMarkAt(maximumLength + 0.5);
}
}
finally
{
_labelPool.Done();
_majorTickMarkPool.Done();
}
}
Listing 2. Original Code for Adding Category Labels
The Panel that we have been discussing about is actually an OrientedPanel class.
This is an internal class found in the same assembly as the chart controls. This
Panel is specialized in that it tries to plot elements in a one-dimensional plane
but minimizes collision of these elements according to some priority. You can
see this at work in Figure 1, where the category labels are plotted in 2 lines.
Since the OrientedPanel is internal, we can’t use or inherit from it so we won’t
be able to wrap it inside a ScrollViewer. The AxisGrid containing this OrientedPanel,
although referenced in one of the ancestors of ScrollableCategoryAxis, is not
accessible in our code by default. We have to override the OnApplyTemplate method
to get a reference to the Grid.
/// <summary>
/// Retrieves template parts and configures layout.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
axisGrid = GetTemplateChild(AxisGridName) as Grid;
if (axisGrid != null)
{
axisGrid.SetValue(Grid.IsSharedSizeScopeProperty, true);
categoryLabelsGrid = new Grid();
axisScrollViewer = new ScrollViewer();
axisScrollViewer.Content = categoryLabelsGrid;
axisScrollViewer.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
axisScrollViewer.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
axisGrid.Children.Add(axisScrollViewer);
}
}
Listing 3. Adding a ScrollViewer
I decided to use a normal Grid to display the category labels. This Grid will be
the content of the ScrollViewer, which will then be added to the AxisGrid. We
set the IsSharedSizeScope property of the AxisGrid to true because I intend to
place category labels in different grid columns having the same width. Next,
we have to override the Render method, which is responsible for adding the category
labels. Note that in the CategoryAxis’ RenderOriented method that you saw in
Listing 2 is actually called by the Render method.
/// <summary>
/// Renders the axis labels, tick marks, and other visual elements.
/// </summary>
/// <param name="availableSize">The available size.</param>
protected override void Render(Size availableSize)
{
if (categoryLabelsGrid != null)
{
categoryLabelsGrid.Children.Clear();
categoryLabelsGrid.ColumnDefinitions.Clear();
foreach (object category in Categories)
{
ColumnDefinition columnDefinition = new ColumnDefinition();
columnDefinition.SharedSizeGroup = "Group";
categoryLabelsGrid.ColumnDefinitions.Add(columnDefinition);
Label categoryLabel = new Label();
categoryLabel.Content = category;
categoryLabel.HorizontalAlignment = HorizontalAlignment.Center;
categoryLabel.SetValue(Grid.ColumnProperty, Categories.IndexOf(category));
categoryLabelsGrid.Children.Add(categoryLabel);
}
}
}
Listing 4. Adding Category Labels
I ignored putting tick marks so that the Render method code is easier to understand.
You can see that there is a column for every category. Since all ColumnDefinitions
has the same SharedSizeGroup value, all columns will have the same width. This
also means that the longest category label will dictate the width of all columns.
Notice that we won’t need to use the availableSize parameter with this kind of
implementation.
Custom Series
To use the ScrollableCategoryAxis, we have to create a class that inherits from the
ColumnSeries class. The ColumnSeries class has a GetAxes method that uses a CategoryAxis
instance as its horizontal axis. So we have to override this method so that an
instance of ScrollableCategoryAxis is returned instead.
public class ScrollableColumnSeries : ColumnSeries
{
/// <summary>
/// Acquire a horizontal category axis and a vertical linear axis.
/// </summary>
/// <param name="firstDataPoint">The first data point.</param>
protected override void GetAxes(DataPoint firstDataPoint)
{
GetAxes(
firstDataPoint,
(axis) => axis.Orientation == AxisOrientation.X,
() =>
{
ScrollableCategoryAxis categoryAxis = new ScrollableCategoryAxis { Orientation = AxisOrientation.X };
return categoryAxis;
},
(axis) =>
{
IRangeAxis rangeAxis = axis as IRangeAxis;
return rangeAxis != null && rangeAxis.Origin != null && axis.Orientation == AxisOrientation.Y;
},
() =>
{
IRangeAxis rangeAxis = CreateRangeAxisFromData(firstDataPoint.DependentValue);
rangeAxis.Orientation = AxisOrientation.Y;
if (rangeAxis == null || rangeAxis.Origin == null)
{
throw new InvalidOperationException("No suitable axis is available for plotting the dependent value.");
}
DisplayAxis axis = rangeAxis as DisplayAxis;
if (axis != null)
{
axis.ShowGridLines = true;
}
return rangeAxis;
});
}
}
Listing 5. Custom ColumnSeries
The following XAML shows how to use the series in an actual chart. This is the same
XAML for the window in Figure 1, except that I used ScrollableColumnSeries instead
of ColumnSeries.
<Window x:Class="WPFToolkitChartScrollDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:WPFToolkitChartScrollControls;assembly=WPFToolkitChartScrollControls"
xmlns:charting="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"
Title="MainWindow" Height="400" Width="500">
<Grid>
<charting:Chart>
<charting:Chart.Series>
<controls:ScrollableColumnSeries
IndependentValueBinding="{Binding Name}"
DependentValueBinding="{Binding Quantity}"
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window},
Path=Parts}"/>
</charting:Chart.Series>
</charting:Chart>
</Grid>
</Window>
Listing 6. Using ScrollableColumnSeries in a Chart
When we run the application, the following figure is shown.

Figure 3. ScrollableCategoryAxis in Action
Now, we can scroll over the category labels. But now, the data points are missing.
So what happened here? The DataPointSeries class is an ancestor of our ScrollableColumnSeries
class. It has an abstract method called UpdateDataPoint. This method is called
when plotting a data point. The ColumnSeries class provides an implementation
of the UpdateDataPoint method. To get the range where the data point can be plotted,
it calls the following method of the ColumnBarBaseSeries class.
/// <summary>
/// Gets a range in which to render a data point.
/// </summary>
/// <param name="category">The category to retrieve the range for.
/// </param>
/// <returns>The range in which to render a data point.</returns>
protected Range<UnitValue> GetCategoryRange(object category)
{
ICategoryAxis categoryAxis = ActualIndependentAxis as CategoryAxis;
if (categoryAxis != null)
{
return categoryAxis.GetPlotAreaCoordinateRange(category);
}
else
{
UnitValue unitValue = ActualIndependentAxis.GetPlotAreaCoordinate(category);
if (ValueHelper.CanGraph(unitValue.Value) && _dataPointlength.HasValue)
{
double halfLength = _dataPointlength.Value / 2.0;
return new Range<UnitValue>(
new UnitValue(unitValue.Value - halfLength, unitValue.Unit),
new UnitValue(unitValue.Value + halfLength, unitValue.Unit));
}
return new Range<UnitValue>();
}
}
Listing 7. GetCategoryRange Method of ColumnBarBaseSeries
Notice here that the ActualIndependentAxis property is converted to the type CategoryAxis,
before it is assigned to a variable of type ICategoryAxis. Since our ScrollableCategoryAxis
class does not inherit from CategoryAxis, its GetPlotAreaCoordinateRange method
is not called even though it implements the ICategoryAxis interface. To work
around this, we can override the UpdateDataPoint method in the ScrollableColumnSeries,
and call the ScrollableCategoryAxis’ GetPlotAreaCoordinateRange method. The following
listing shows the updated ScrollableColumnSeries code.
public class ScrollableColumnSeries : ColumnSeries
{
private Panel plotArea;
/// <summary>
/// Get the plot area.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
plotArea = GetTemplateChild(DataPointSeries.PlotAreaName) as Panel;
}
/// <summary>
/// Acquire a horizontal category axis and a vertical linear axis.
/// </summary>
/// <param name="firstDataPoint">The first data point.</param>
protected override void GetAxes(DataPoint firstDataPoint)
{
// Omitted for readability
// ...
}
/// <summary>
/// Updates each point.
/// </summary>
/// <param name="dataPoint">The data point to update.</param>
protected override void UpdateDataPoint(DataPoint dataPoint)
{
if (SeriesHost == null || plotArea == null)
{
return;
}
object category = dataPoint.ActualIndependentValue ?? (this.ActiveDataPoints.IndexOf(dataPoint) + 1);
ICategoryAxis categoryAxis = ActualIndependentAxis as ScrollableCategoryAxis;
Range<UnitValue> coordinateRange = categoryAxis.GetPlotAreaCoordinateRange(category);
// Omitted for readability
// ...
}
}
Listing 8. Workaround for Incorrect Data Point Ranges
I copied the UpdateDataPoint implementation in the ColumnSeries class and made a
couple of changes to make things work. You can see that instead of calling the
GetCategoryRange shown in Listing 7, I directly converted the ActualIndependentAxis
to ScrollableCategoryAxis. This time, it will not return null and the GetPlotAreaCoordinateRange
will be called.

Figure 4. Data Points Get Plotted
Now, data points are plotted the same way as we see in Figure 1. The next thing to
do is to plot each data point according to its category’s position in the axis.
Plotting Data Points
To plot each data point correctly, we have to modify the GetPlotAreaCoordinateRange
method of the ScrollableCategoryAxis class. The following code listing shows
the unmodified GetPlotAreaCoordinateRange method.
/// <summary>
/// Returns range of coordinates for a given category.
/// </summary>
/// <param name="category">The category to return the range for.</param>
/// <returns>The range of coordinates corresponding to the category.
/// </returns>
public Range<UnitValue> GetPlotAreaCoordinateRange(object category)
{
// Omitted for readability
// ...
if (Orientation == AxisOrientation.X || Orientation == AxisOrientation.Y)
{
double maximumLength = Math.Max(ActualLength - 1, 0);
double lower = (index * maximumLength) / Categories.Count;
double upper = ((index + 1) * maximumLength) / Categories.Count;
if (Orientation == AxisOrientation.X)
{
return new Range<UnitValue>(new UnitValue(lower, Unit.Pixels), new UnitValue(upper, Unit.Pixels));
}
else if (Orientation == AxisOrientation.Y)
{
return new Range<UnitValue>(new UnitValue(maximumLength - upper, Unit.Pixels), new UnitValue(maximumLength - lower, Unit.Pixels));
}
}
else
{
// Omitted for readability
// ...
}
return new Range<UnitValue>();
}
Listing 9. ScrollableCategoryAxis’ GetPlotAreaCoordinateRange Method
You can see in the preceding code listing a variable called maximumLength. This variable
indicates the width or height of the AxisGrid. The length is then divided into
the number of categories. In our scenario, this would be incorrect since the
Grid containing the category labels is longer than the AxisGrid. Thus, we should
use the ActualWidth or ActualHeight properties of the categoryLabelsGrid instead
of the AxisGrid. We just need to add the following property.
/// <summary>
/// Gets the actual length.
/// </summary>
protected new double ActualLength
{
get
{
if (categoryLabelsGrid != null)
{
return GetLength(new Size(categoryLabelsGrid.ActualWidth, categoryLabelsGrid.ActualHeight));
}
return base.ActualLength;
}
}
Listing 10. Return the Correct Axis Length
Let’s run the application again and check if the data points are plotted correctly.

Figure 5. Correct Axis Length
It looks better now but resizing the window gives us the following result.

Figure 6. Positions and Widths Gone Wrong
This time, the positions of the data points are incorrect. Also, the widths of the
data points seem slightly smaller when compared to those in Figure 5. After some
debugging, I found out that the value of the ActualWidth property of the categoryLabelsGrid
changes when the window is resized. I don’t know why exactly but I believe it
has something to do with WPF’s layout system. Anyway, a simple fix is to store
the actual size of the Grid when it is loaded and use that size in the ActualLength
property we defined earlier.
/// <summary>
/// Stores the size of the Grid containing the category labels.
/// </summary>
/// <param name="sender">Grid.</param>
/// <param name="e">Ignored.</param>
private void CategoryLabelsGrid_Loaded(object sender, RoutedEventArgs e)
{
gridSize = new Size(categoryLabelsGrid.ActualWidth, categoryLabelsGrid.ActualHeight);
// Force redraw
Invalidate();
}
/// <summary>
/// Gets the actual length.
/// </summary>
protected new double ActualLength
{
get
{
if (gridSize != null)
{
return GetLength(gridSize);
}
return base.ActualLength;
}
}
Listing 11. Fix for Incorrect Positions and Widths
Last thing to do is to handle the ScrollChanged event of the ScrollViewer to display
the data points that should be visible.
Handling Scrolling
Our problem with scrolling is that the ScrollViewer is defined in the ScrollableCategoryAxis
class while the UpdateDataPoints method can be called in the ScrollableColumnSeries
class. I thought of a simple solution: define an event in ScrollableCategoryAxis
that will be raised when the ScrollViewer has been scrolled.
/// <summary>
/// Triggers a ScrollChanged event.
/// </summary>
/// <param name="sender">ScrollViewer.</param>
/// <param name="e">Ignored.</param>
private void AxisScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
EventHandler eh = ScrollChanged;
if (eh != null)
{
eh(this, new EventArgs());
}
}
Listing 12. Notify ScrollableColumnSeries of Change in ScrollViewer
Then, in the GetAxes method of ScrollableColumnSeries, we can hook up the event handler
to the ScrollChanged event of the ScrollableCategoryAxis instance. In the event
handler, we can call the UpdateDataPoints method.
/// <summary>
/// Updates the data points when the scrollbar has been moved.
/// </summary>
/// <param name="sender">ScrollableCategoryAxis object.</param>
/// <param name="e">Ignored.</param>
private void ScrollableCategoryAxis_ScrollChanged(object sender, EventArgs e)
{
UpdateDataPoints(ActiveDataPoints);
}
Listing 13. ScrollChanged Event Handler
We then need to update the GetPlotAreaCoordinateRange method of the ScrollableCategoryAxis
to take into consideration the ScrollViewer’s horizontal or vertical offset.
double offset = 0.0;
if (axisScrollViewer != null)
{
offset = Orientation == AxisOrientation.X ? axisScrollViewer.HorizontalOffset :
axisScrollViewer.VerticalOffset;
}
double maximumLength = Math.Max(ActualLength - 1, 0);
double lower = (index * maximumLength) / Categories.Count - offset;
double upper = ((index + 1) * maximumLength) / Categories.Count - offset;
Listing 14. ScrollChanged Event Handler
Let’s run again the application. You can see that it works as expected.

Figure 7. A Scrollable Column Chart
You can download the Visual Studio 2008 solution here.