Click here to Skip to main content
13,142,065 members (56,896 online)
Click here to Skip to main content
Add your own
alternative version

Stats

37.7K views
18 bookmarked
Posted 3 May 2009

A Stacked Bar Chart Silverlight control

, 3 May 2009
Rate this:
Please Sign up or sign in to vote.
A stacked Bar Chart control for Silverlight.

The Silverlight Toolkit provides developers and designers with a whole host of different controls that don't come as standard. Among other things, it contains charting controls for creating bar charts, pie charts, and line graphs etc. I recently had a requirement for a stacked bar chart, which unfortunately is one of the few types of charts that isn't supported out of the box. However, after reading a series of posts written by Jafar Husain and taking a look at the toolkit source code, I realised that I could create a stacked bar chart relatively easily myself - despite having only limited knowledge of Silverlight. At this point, I have to apologise to Jafar - I asked his advice via his blog, and he responded with a very comprehensive description of how he would go about tackling this problem. Jafar's approach would have produced a much more complete and flexible solution; however, it was overkill for what I needed, and I had to get something put together as quickly as possible, so I took an (arguably) more hacky approach.

The approach I took was to create a new StackedColumnSeries which is a total copy of the System.Windows.Controls.DataVisualization.Charting.ColumnSeries class. At this point, it would have been great to have been able to use ColumnSeries as the base class for StackedColumnSeries, but because ColumnSeries is sealed, I had to copy and paste instead. I had to copy and paste various other chunks of code from the toolkit as well, to get my copy of ColumnSeries to compile since various helper classes etc were internal. If you download the code you will see that I've clearly commented which bits of code I've copied, so if the toolkit team open up their code a bit more in the future it will be straight forward to change the StackedColumnSeries class.

Once the StackedColumnSeries class was compiling and functioning like the ColumnSeries, I needed to make changes to just two methods to make it function like a stacked column chart. The methods that I changed were UpdateDataPoint(DataPoint dataPoint) so that columns could be rendered so that they appear stacked on top of each other, and GetValueMargins(IValueMarginConsumer consumer) so that I could increase the range of values that appear on the axis, taking into account the fact that the maximum value that needed to be displayed now needed to be the sum of the stacked columns.

The two changed methods are shown below - please see the comments in the code for the changes that I needed to make to the code that I copied from the ColumnSeries.

/// <span class="code-SummaryComment"><summary>
</span>/// Updates each point.
/// <span class="code-SummaryComment"></summary>
</span>/// <span class="code-SummaryComment"><param name="dataPoint">The data point to update.</param>
</span>protected override void UpdateDataPoint(DataPoint dataPoint)
{
    if (SeriesHost == null || PlotArea == null)
    {
        return;
    }

    object category = dataPoint.IndependentValue ?? 
          (this.ActiveDataPoints.IndexOf(dataPoint) + 1);
    Range<UnitValue> coordinateRange = 
                           GetCategoryRange(category);
    if (!coordinateRange.HasData)
    {
        return;
    }
    else if (coordinateRange.Maximum.Unit != Unit.Pixels || 
             coordinateRange.Minimum.Unit != Unit.Pixels)
    {
        throw new InvalidOperationException("This Series " + 
                  "Does Not Support Radial Axes");
    }

    double minimum = (double)coordinateRange.Minimum.Value;
    double maximum = (double)coordinateRange.Maximum.Value;

    double plotAreaHeight = ActualDependentRangeAxis.GetPlotAreaCoordinate(
                            ActualDependentRangeAxis.Range.Maximum).Value.Value;
    IEnumerable<StackedColumnSeries> columnSeries = 
      SeriesHost.Series.OfType<StackedColumnSeries>().Where(series => 
      series.ActualIndependentAxis == ActualIndependentAxis);
    int numberOfSeries = columnSeries.Count();
    double coordinateRangeWidth = (maximum - minimum);
    double segmentWidth = coordinateRangeWidth * 0.8;

    // For stacked chart, no need to scale
    // the width of the columns if there are more series
    double columnWidth = segmentWidth; // / numberOfSeries;
    int seriesIndex = columnSeries.IndexOf(this);

    double dataPointY = ActualDependentRangeAxis.GetPlotAreaCoordinate(
           ValueHelper.ToDouble(dataPoint.ActualDependentValue)).Value.Value;
    double zeroPointY = ActualDependentRangeAxis.GetPlotAreaCoordinate(
                        ActualDependentRangeAxis.Origin).Value.Value;

    // Need to shift the columns up to take account of the other
    // columns that are already rendered, to make them
    // appear stacked
    int dataPointIndex = ActiveDataPoints.IndexOf(dataPoint);

    for (int i = numberOfSeries - 1; i > seriesIndex; i--)
    {
        StackedColumnSeries prevSeries = columnSeries.ElementAt<StackedColumnSeries>(i);

        if (prevSeries.ActiveDataPointCount >= dataPointIndex + 1)
        {
            double yOffset = ActualDependentRangeAxis.GetPlotAreaCoordinate(
                   ValueHelper.ToDouble(prevSeries.ActiveDataPoints.ElementAt<DataPoint>(
                   dataPointIndex).ActualDependentValue)).Value.Value;

            dataPointY += yOffset;
            zeroPointY += yOffset;
        }
    }

    // No offset for stacked bar charts so that all the columns line up
    double offset = 0; /*seriesIndex * Math.Round(columnWidth) + 
                         coordinateRangeWidth * 0.1;*/
    double dataPointX = minimum + offset;

    if (GetIsDataPointGrouped(category))
    {
        // Multiple DataPoints share this category;
        // offset and overlap them appropriately
        IGrouping<object, DataPoint> categoryGrouping = 
                                     GetDataPointGroup(category);
        int index = categoryGrouping.IndexOf(dataPoint);
        dataPointX += (index * (columnWidth * 0.2)) / 
                      (categoryGrouping.Count() - 1);
        columnWidth *= 0.8;
        Canvas.SetZIndex(dataPoint, -index);
    }

    if (ValueHelper.CanGraph(dataPointY) && 
        ValueHelper.CanGraph(dataPointX) && 
        ValueHelper.CanGraph(zeroPointY))
    {
        // Remember, the coordinate 0,0 is in the top left hand corner,
        // therefore the "top" y coordinate is going to
        // be a smaller value than the bottom.
        double left = Math.Round(dataPointX);
        double width = Math.Round(columnWidth);

        double top = Math.Round(plotAreaHeight - 
                     Math.Max(dataPointY, zeroPointY) + 0.5);
        double bottom = Math.Round(plotAreaHeight - 
                        Math.Min(dataPointY, zeroPointY) + 0.5);
        double height = bottom - top + 1;

        Canvas.SetLeft(dataPoint, left);
        Canvas.SetTop(dataPoint, top);
        dataPoint.Width = width;
        dataPoint.Height = height;
    }
}

/// <span class="code-SummaryComment"><summary>
</span>/// Returns the value margins for a given axis.
/// <span class="code-SummaryComment"></summary>
</span>/// <span class="code-SummaryComment"><param name="consumer">The axis to retrieve the value margins for.
</span>/// <span class="code-SummaryComment"></param>
</span>/// <span class="code-SummaryComment"><returns>A sequence of value margins.</returns>
</span>protected override IEnumerable<ValueMargin> 
                   GetValueMargins(IValueMarginConsumer consumer)
{
    double dependentValueMargin = this.ActualHeight / 10;
    IAxis axis = consumer as IAxis;
    if (axis != null && ActiveDataPoints.Any())
    {
        Func<DataPoint, IComparable> selector = null;
        if (axis == InternalActualIndependentAxis)
        {
            selector = (dataPoint) => (IComparable)dataPoint.ActualIndependentValue;

            DataPoint minimumPoint = ActiveDataPoints.MinOrNull(selector);
            DataPoint maximumPoint = ActiveDataPoints.MaxOrNull(selector);

            double minimumMargin = minimumPoint.GetMargin(axis);
            yield return new ValueMargin(selector(minimumPoint), 
                             minimumMargin, minimumMargin);

            double maximumMargin = maximumPoint.GetMargin(axis);
            yield return new ValueMargin(selector(maximumPoint), 
                             maximumMargin, maximumMargin);
        }
        else if (axis == InternalActualDependentAxis)
        {
            // Some up the total value of the current set of columns
            double valueTotal = 0.0;

            foreach (DataPoint dataPoint in 
                     ActiveDataPoints.AsEnumerable<DataPoint>())
            {
                valueTotal += (double)dataPoint.ActualDependentValue;
            }
            // The minimum value always needs to be 0 for stacked column charts
            yield return new ValueMargin(0.0, dependentValueMargin, 
                                         dependentValueMargin);
            yield return new ValueMargin(valueTotal, dependentValueMargin, 
                                         dependentValueMargin);
        }
    }
    else
    {
        yield break;
    }
}

The result of running this code and binding some data to the chart can be shown below (notice, I have also added a line series):

Staked Bar Chart

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

S1mm0t
United Kingdom United Kingdom
No Biography provided

You may also be interested in...

Pro

Comments and Discussions

 
GeneralExcellent article thanks for sharing, however...... Pin
Alan Gregory22-Feb-10 21:10
memberAlan Gregory22-Feb-10 21:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.170915.1 | Last Updated 3 May 2009
Article Copyright 2009 by S1mm0t
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid