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

Simple WPF Bar Chart Control

0.00/5 (No votes)
4 Oct 2008 1  
This article presents step-by-step instructions on how to create a simple bar chart using WPF.
Chart 1

Introduction

This article presents step-by-step instructions on how to create a simple bar chart with WPF, using C#. You could substitute C# with any other .NET language of your choice. This article is only intended as a learning exercise. The chart is not of professional quality. There may be bugs here and there, but there are also a few good points. I'll keep improving this code over a period as I incrementally build my WPF skills, and hope to post more articles.

The current code is written to work with XML data. You could replace this with minimum effort. In a later article, I will try to improve upon this and make this more generic as the intent of this article was just to get hooked on to the charting capabilities of WPF using basic elements.

I am thankful to a few of my colleagues in my present company [not naming them as I haven't taken their permission], who in some form or the other enriched my understanding (whatever little I have) of WPF.

Background

This article assumes prior knowledge of C# and WPF. In case you are interested in learning about WPF, there are some excellent resources available on The Code Project. A few of them are listed below:

The skeleton UML class diagram to represent the BarChart control is shown below:

imgUML

To run the attached demo, please make sure the following config entries are valid for your environment:

<appSettings>
    <add key="SALES_XML"
            value="C:\SimpleChart\SimpleChart\Data\SalesSummary.xml"/>
    <add key="SALES1_XML"
            value="C:\SimpleChart\SimpleChart\Data\SalesSummary1.xml"/>
</appSettings>

Using the Code

Let's have a look at how this BarChart could be used in your application to create a single valued chart.

chart1 is an instance of BarChart:

// Resets the values
chart1.Reset();

// Setting this property provides angled dimension
// to the X-axis for more readability
chart1.SmartAxisLabel = true;

// Set the chart title
chart1.Title = "Sales by Region";

// Set the Y-Axis value
chart1.ValueField.Add("Amount");

// Set the chart tooltip. {field} will be replaced by current bar value.
// Need to improve in this little templating.
chart1.ToolTipText = "Sales($): {field}";

// Set the x-axis text
chart1.XAxisText = "Sales";

// Set the x-axis data field
chart1.XAxisField = "Region";

// Setting this value displays the bar value on top of the bar.
chart1.ShowValueOnBar = true;

// Get the data required for displaying the graph.
DataSet dsetAll = QueryHelper.GetDataSet(
        ConfigurationManager.AppSettings["SALES_XML"]);

// Set the datasource
chart1.DataSource = dsetAll;

// Generate the Graph
chart1.Generate();

To create a multi-valued bar chart, use the snippet below:

// Reset the chart
chart2.Reset();

// Setting this property provides angled
// dimension to the X-axis for more readability
chart2.SmartAxisLabel = true;

// Set the chart title
chart2.Title = "Sales Summary by Region";

// Add the range of values to the chart.
chart2.ValueField.Add("Min");
chart2.ValueField.Add("Max");
chart2.ValueField.Add("Avg");

// Set the chart tooltip.
// {field} will be replaced by current bar value.
// Need to improve in this little templating.
chart2.ToolTipText = "Sales($): {field}";

// Set the x-axis text
chart2.XAxisText = "Sales";

// Set the x-axis field
chart2.XAxisField = "Region";

// Setting this value displays the bar value on top of the bar.
chart2.ShowValueOnBar = true;

// Get the data
dsetAll = QueryHelper.GetDataSet(
  ConfigurationManager.AppSettings["SALES1_XML"]);

// Assign to datasource
chart2.DataSource = dsetAll;

// Generate the chart
chart2.Generate();

BarChart Dissected

The XAML representation of BarChart.xaml is as follows:

<UserControl x:Class="SimpleChart.Charts.BarChart"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     Height="640" Width="480">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="0.05*"></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
            <Slider Name="zoomSlider" Grid.Row="0" Grid.Column="1" Height="20"
            DockPanel.Dock="Top" Minimum="1" Maximum="5" Value="1"/>
            <ScrollViewer Grid.Row="1" Grid.Column="1" CanContentScroll="True"
              HorizontalScrollBarVisibility="Auto"
              VerticalScrollBarVisibility="Auto">
            <Canvas  x:Name="chartArea" ClipToBounds="False" Background="Black">
                <Canvas.LayoutTransform>
                     <ScaleTransform
                          ScaleX="{Binding ElementName=zoomSlider, Path=Value}"
                          ScaleY="{Binding ElementName=zoomSlider, Path=Value}"/>
                </Canvas.LayoutTransform>
                <TextBlock  x:Name="txtNoData" FontSize="12"
                      Text="No data found." Visibility="Hidden"
                      Opacity="100" Foreground="Red">
                </TextBlock>
            </Canvas>
        </ScrollViewer>
    </Grid>
</UserControl>

Setting up the Chart

We set up the basic chart layout by adding an X-axis, a top title, and a bubble text:

/// <summary>
/// Add chart controls to chart.
/// This creates a basic layout for the chart.
/// </summary>
private void AddChartControlsToChart()
{
    chartArea.Children.Add(txtXAxis);
    chartArea.Children.Add(txtTopTitle);
    chartArea.Children.Add(bubbleText);
}

Drawing the Chart

The main drawing activity happens in the Generate method. This mind-map shows the important activities for drawing a chart:

imgMM

  • Add chart controls to setup the basic layout.
  • Set up chart area.
  • Get the data source.
  • Check if data is present. If not, draw empty chart.
  • Calculate the max Y-axis value. This is required for proper scaling.
  • Draw the X-axis.
  • Draw the Y-axis.
  • Draw the X-axis label.
  • Draw bar for each data value.
  • Draw the legend.
/// <summary>
/// Creates the chart based on the datasource.
/// </summary>
public void Generate()
{
    // Reset / Clear
    legends.Clear();
    chartArea.Children.Clear();

    // Setup chart elements.
    AddChartControlsToChart();

    // Setup chart area.
    SetUpChartArea();

    // Will be made more generic in the next versions.
    DataTable dt = (DataSource as DataSet).Tables[0];

    if (null != dt)
    {
        // if no data found draw empty chart.
        if (dt.Rows.Count == 0)
        {
            DrawEmptyChart();
            return;
        }

        // Hide the nodata found text.
        txtNoData.Visibility = Visibility.Hidden;

        // Get the max y-value.
        // This is used to calculate the scale and y-axis.
        maxData = GetMax(dt);

        // Prepare the chart for rendering.
        // Does some basic setup.
        PrepareChartForRendering();

        // Get the total bar count.
        int barCount = dt.Rows.Count;

        // If more than 1 value field, then this is a group chart.
        bool isSeries = ValueField.Count > 1;

        // no legends added yet.
        bool legendAdded = false;  // no legends yet added.

        // For each row in the datasource
        foreach (DataRow row in dt.Rows)
        {
            // Draw x-axis label based on datarow.
            DrawXAxisLabel(row);

            // Set the barwidth. This is required to adjust
            // the size based on available no. of bars.
            SetBarWidth(barCount);

            // For each row the current series is initialized
            // to 0 to indicate start of series.
            int currentSeries = 0;

            // For each value in the datarow, draw the bar.
            foreach (string valField in ValueField)
            {
                if (null == valField)
                    continue;

                if (!row.Table.Columns.Contains(valField))
                    continue;

                // Draw bar for each value.
                DrawBar(isSeries, legendAdded, row,
                        ref currentSeries, valField);

            }
            legendAdded = true;

            // Set up location for next bar in series.
            if (isSeries)
                left = left + spaceBetweenBars;
        }

        // Reset the chartarea to accommodate all the chart elements.
        if ((left + BarWidth) > chartArea.Width)
            chartArea.Width = left + BarWidth;

	  // Draw the x-axis.
        DrawXAxis();

        // Draw the y-axis.
        DrawYAxis();

        // Draw the legend.
        DrawLegend();
    }
}

Drawing the Bar

The DrawBar() function handles the rendering of each bar. The bar, in our case, is represented by a rectangle object.

/// <summary>
/// Draws a bar
/// </summary>
/// <param name=""""""isSeries"""""">Whether current bar
///             is in a series or group.</param>
/// <param name=""""""legendAdded"""""">Indicates whether to add legend.</param>
/// <param name=""""""row"""""">The current bar row.</param>
/// <param name=""""""currentSeries"""""">The current series.
///             Used to group series and color code bars.</param>
/// <param name=""""""valField"""""">Value is fetched from
///             the datasource from this field.</param>
private void DrawBar(bool isSeries, bool legendAdded, DataRow row,
                     ref int currentSeries, string valField)
{
    double val = 0.0;

    if (row[valField] == DBNull.Value)
        val = 0;
    else
        val = Convert.ToDouble(row[valField]);

    // Calculate bar value.
    double? calValue = (((float)val * 100 / maxData)) *
            (chartArea.Height - bottomMargin - topMargin) / 100;

    Rectangle rect = new Rectangle();

    // Setup bar attributes.
    SetBarAttributes(calValue, rect);

    // Color the bar.
    Color stroke = Helper.GetDarkColorByIndex(currentSeries);
    rect.Fill = new SolidColorBrush(stroke);

    // Setup bar events.
    SetBarEvents(rect);

    // Add the legend if not added.
    if (isSeries && !legendAdded)
    {
        legends.Add(new Legend(stroke, ValueField[currentSeries]));
    }

    // Calculate bar top and left position.
    top = (chartArea.Height - bottomMargin) - rect.Height;
    Canvas.SetTop(rect, top);
    Canvas.SetLeft(rect, left + leftMargin);

    // Add bar to chart area.
    chartArea.Children.Add(rect);

    // Display value on bar if set to true.
    if (ShowValueOnBar)
    {
        DisplayYValueOnBar(val, rect);
    }

    // Create Bar object and assign to the rect.
    rect.Tag = new Bar(val, row, valField);

    // Calculate the new left position for subsequent bars.
    if (isSeries)
        left = left + rect.Width;
    else
        left = left + BarWidth + spaceBetweenBars;

    // Increment the series
    currentSeries++;
}

Drawing X-Axis

Setting up the X-axis is quite simple. Just add the line control at the appropriate location:

/// <summary>
/// Draws XAxis
/// </summary>
private void DrawXAxis()
{
    // Draw axis
    Line xaxis = new Line();
    xaxis.X1 = leftMargin;
    xaxis.Y1 = this.Height - bottomMargin;
    xaxis.X2 = this.chartArea.Width ;
    xaxis.Y2 = this.Height - bottomMargin;

    xaxis.Stroke = new SolidColorBrush(Colors.Silver);
    chartArea.Children.Add(xaxis);
}

Drawing Y-Axis

Setting up the Y-axis is quite simple. The following activities have to be done in order to set up the Y-axis:

  • Draw the Y-axis.
  • Calculate the Y-axis marker value.
  • Calculate the Y-axis scale.
  • Add the marker line and marker text for each scale factor.
/// <summary>
/// Draws YAxis. Here we use the maxData value calculated earlier.
/// This method also sets up the y-axis marker.
/// </summary>
private void DrawYAxis()
{
    // Drawing yaxis is as simple as adding
    // a line control at appropriate location.
    Line yaxis = new Line();
    yaxis.X1 = leftMargin;
    yaxis.Y1 = 0;
    yaxis.X2 = leftMargin;
    yaxis.Y2 = this.Height - bottomMargin;
    yaxis.Stroke = new SolidColorBrush(Colors.Silver);
    chartArea.Children.Add(yaxis);

    // Set the scale factor for y-axis marker.
    double scaleFactor = 10;

    // this value is used to increment the y-axis marker value.
    double yMarkerValue = Math.Ceiling(maxData.Value / scaleFactor);

    // This value is used to increment the y-axis marker location.
    double scale = 5;  // default value 5.

    // get the scale based on the current max y value
    // and other chart element area adjustments.
    scale = (((float)yMarkerValue * 100 / maxData.Value)) *
        (chartArea.Height - bottomMargin - topMargin) / 100;

    double y1 = this.Height - bottomMargin;

    double yAxisValue = 0;

    for (int i = 0; i <= scaleFactor; i++)
    {
        // Add y-axis marker line chart.
        Line marker = AddMarkerLineToChart(y1);

        // Draw horizontal grid based on marker location.
        DrawHorizontalGrid(marker.X1, y1);

        // Add the y-marker to the chart.
        AddMarkerTextToChart(y1, yAxisValue);

        // Adjust the top location for next marker.
        y1 -= scale;

        // Increment the y-marker value.
        yAxisValue += yMarkerValue;
    }
}

Drawing the Legend

Drawing the legend is pretty straightforward. Loop through the Legends collection and draw a line and text at the appropriate location with the specified colors.

/// <summary>
/// Draw chart legends.
/// </summary>
private void DrawLegend()
{
    if (legends == null || legends.Count == 0)
        return;

    // Initialize legend location.
    double legendX1 = leftMargin + txtXAxis.Text.Length + 100;
    double legendWidth = 20;

    // Draw all legends
    foreach (Legend legend in legends)
    {
        Line legendShape = new Line();

        legendShape.Stroke = new SolidColorBrush(legend.LegendColor);
        legendShape.StrokeDashCap = PenLineCap.Round;
        legendShape.StrokeThickness = 8;

        legendShape.StrokeStartLineCap = PenLineCap.Round;
        legendShape.StrokeEndLineCap = PenLineCap.Triangle;


        legendShape.X1 = legendX1;
        legendShape.Y1 = this.Height - 10;
        legendShape.X2 = legendX1 + legendWidth;
        legendShape.Y2 = this.Height - 10;

        chartArea.Children.Add(legendShape);

        TextBlock txtLegend = new TextBlock();
        txtLegend.Text = legend.LegendText;
        txtLegend.Foreground = legendTextColor;

        chartArea.Children.Add(txtLegend);
        Canvas.SetTop(txtLegend, this.Height - 20);
        Canvas.SetLeft(txtLegend, legendShape.X2 + 2);

        legendX1 += legendWidth + 30 + txtLegend.Text.Length;
    }
}

Feedback

The above code is a result of my learning WPF. In no means is this code optimized or perfect in every sense. The idea was to just post what I learned so that others may benefit from it. Please provide constructive criticism as this will help me improve my WPF skills and help me write better articles.

Points of Interest

Some of the things which I learned while developing this hobby code are outlined below:

  • Understand basic chart drawing
  • Understand basic WPF layout
  • How to draw a smart axis label
  • Tooltip and templating (under development)
  • Adding legends
  • Zoom-in/Zoom-out features

Development Ideas

Highlighted below are some of the objectives which I would like to tackle in my next article:

  • Make the chart component reusable
  • Support any datasource
  • Enhanced tooltip templating
  • Drilldown features
  • In-built chart filtering capabilities

I may not be able to meet all the above intent in one go, but would take it up incrementally over a period of time.

Credits

All credits to CodeProject WPF article writers as I am learning mostly from their articles.

History

  • 28th September, 2008 - First release (unorganized and probably buggy, but a learning exercise)
  • 1st October, 2008 - Minor bug fixes

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