Introduction
I have been building many dashboards for clients, and recently upgraded to Silverlight 4 and its corresponding Toolkit version. I went straight to the stacking
features of the charts, and found a very surprising thing. If you add more than one StackedColumnSeries
, only one column would show.
The reason this is surprising is that the non-stacking version of the ColumnSeries
does group nicely.
Since grouping (or clustering as Excel would say) stacking columns together is very useful in comparing groups of information against each other without losing granularity,
I decided to add this missing feature. It turned out to be an extremely simple change, but provides a sleek upgrade.
Charting Architecture
A Chart
is made up of one or more series. Normally, each series would be a single line or column etc. The design of the charting system is very flexible,
so a custom series can basically be anything it wants to be. In order to be this flexible, the chart looks at the series definitions to help determine the axis details and range.
Then it goes through the defined series and has them draw themselves.
Like all controls placed in the same container, they are drawn in the order they get declared. So when there are multiple series that want to occupy the same screen area,
only the last one can be seen.
The stacked series framework adds a new layer of abstraction to the setup. Instead of just adding a ColumnSeries
into the chart, you add a StackedColumnSeries
.
Into that, you define the actual series that gets stacked together in each category.
This StackedColumnSeries
then manages all the defined series and DataItem
s and configures where they are drawn. And since its implementation stretches each
column across the entire width assigned to each category, if there are multiple declared, only one will be seen.
The Upgrade
Since the StackedColumnSeries
class handles the positioning of DataItem
s, I inherited from it, and overrode the UpdateDataItemPlacement
method.
I basically copied this method and the required private and internal dependencies from the Toolkit's source code. Then updated the position and width each DataItem
was assigned.
The first part is to find out how many groups there were going to be and which Series was being configured.
var numSeries = this.SeriesHost.Series.Count;
int index = this.SeriesHost.Series.IndexOf(this);
Then update the rendering details based on that:
width = width / numSeries;
leftCoordinate = leftCoordinate + (width * index);
Using the Code
With my upgrade, all you have to do is define more than one GroupedStackedColumnSeries
in the chart. Every one of these defines a separate column of stacked values.
The setup below creates two sets of stacked columns. Comparing Net (realized and projected) against Gross (realized and projected) for each category.
<chartingToolkit:Chart x:Name="chart" BorderThickness="0" >
<chartingToolkit:Chart.Series>
<sdk:GroupedStackedColumnSeries >
<chartingToolkit:SeriesDefinition Title="Net (Projected)"
IndependentValueBinding="{Binding Name}" DependentValueBinding="{Binding Value}" />
<chartingToolkit:SeriesDefinition Title="Net (Realized)"
IndependentValueBinding="{Binding Name}" DependentValueBinding="{Binding Value}" />
<sdk:GroupedStackedColumnSeries >
<chartingToolkit:SeriesDefinition Title="Gross (Projected)"
IndependentValueBinding="{Binding Name}" DependentValueBinding="{Binding Value}" />
<chartingToolkit:SeriesDefinition Title="Gross (Realized)"
IndependentValueBinding="{Binding Name}" DependentValueBinding="{Binding Value}" />
</sdk:GroupedStackedColumnSeries>
</chartingToolkit:Chart.Series>
<chartingToolkit:Chart.Axes>
<chartingToolkit:LinearAxis Orientation="Y"
Minimum="0" Location="Left" Title="Sales" />
<chartingToolkit:CategoryAxis Title="Category" Orientation="X"/>
</chartingToolkit:Chart.Axes>
</chartingToolkit:Chart>
Data is loaded by filling all the SeriesDefinitions
' ItemSource
s. I think of the required data structure to be an array of Key / Value pairs,
one entry for each category item or layer in that column stack.
StackedColumnSeries series = chart.Series[0] as StackedColumnSeries;
series.SeriesDefinitions[0].ItemsSource = e.Result.Result.RealizedNet;
series.SeriesDefinitions[1].ItemsSource = e.Result.Result.ProjectedNet;
series = chart.Series[1] as StackedColumnSeries;
series.SeriesDefinitions[0].ItemsSource = e.Result.Result.RealizedGross;
series.SeriesDefinitions[1].ItemsSource = e.Result.Result.ProjectedGross;
Points of Interest
I added in some selection logic as well, to ensure only one DataPoint
from any series can be selected at once, even if they are in separate columns.
This was just to improve the user experience of the groups.
**Don't set backgrounds - since series layer on top of each other, the last one with a background will hide all the previous ones.
History
- Aug. 6, 2011 - Original article.