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

Simple WPF BarChart (Horizontal and Vertical) - Part II

0.00/5 (No votes)
2 Oct 2008 1  
This article is an extension of the earlier one, and demonstrates simple steps to create a horizontal and vertical bar chart using WPF.

Table of Contents

"A picture is worth a thousand words". So, let's begin by having a look at of the snapshots of the barchart control in action.

Vertical Bar Chart

Chart 1

Horizontal Bar Chart

Chart 1

Introduction

I have taken the task of teaching myself WPF by creating a simple charting library. I have started this by creating a small WPF bar chart control. You can have a look at my first attempt here at CodeProject, available here: Simple WPF BarChart Control.

This was a great learning experience in itself. To further enhance my understating of the topic, I added more features to this charting library, and thought of sharing this with you all.

The following are the new features added, apart from some minor bug fixes:

  • Modified the entire layout to give a sleek look (not so sleek yet).
  • Added horizontal bar charting capability.
  • Added event communication between the chart and the host application.

The current code is still written to work with datasets. You could replace this with minimum effort to support any other data source. The intent of this article is just to get hooked on to the charting capabilities of WPF using the basic elements without getting to other complexities.

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 here in CodeProject. Afew of them are linked below:

The skeleton UML class diagram to represent the bar chart is shown below. There's a small change from the earlier version. Now, we also have a BarEventArgs object which represents the object which gets passed to the host when we clicks on the bar.

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:\Rajesh\MyCodeProject\Lab\
            SimpleChart\SimpleChart\Data\SalesSummary.xml"/>
    <add key="SALES1_XML" 
            value="C:\Rajesh\MyCodeProject\Lab\
            SimpleChart\SimpleChart\Data\SalesSummary1.xml"/>
</appSettings>

Using the Code

Let's have a look at how this bar chart could be used in your application to create a HorizontalBar chart. For detailed attributes, please refer to the first part of this series.

chart1 is an instance of BarChart:

// Set the ChartType to HorizontalBar
chart1.Type = BarChart.ChartType.HorizontalBar;

To create a VerticalBar chart, use the snippet below..

// Set the ChartType to Vertical Bar
chart2.Type = BarChart.ChartType.VerticalBar;

BarChart Dissected

The XAML representation of BarChart.xaml is as follows. This is different from the first version of this library.

<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 x:Name="chartLayout">
        <Grid.RowDefinitions>
            <RowDefinition Height="10*"></RowDefinition>
            <RowDefinition Height="5*"></RowDefinition>
            <RowDefinition Height="80*"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="80*"></ColumnDefinition>
            <ColumnDefinition Width="20*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
            <Slider Name="zoomSlider" Grid.Row="0" 
               Grid.Column="0" Height="20" DockPanel.Dock="Top" 
               Minimum="1" Maximum="5" Value="1"/>
            <Canvas x:Name="chartTitle"  Grid.Row="1" Grid.Column="0">
                <TextBlock  x:Name="txtTopTitle" FontSize="12" Text="Title" 
                          Opacity="100" >
                    </TextBlock>
            </Canvas>
            <Canvas x:Name="chartLegendArea"  Grid.Row="2" Grid.Column="1"></Canvas>
            <ScrollViewer Grid.Row="2" Grid.Column="0" 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>

The controls are organised in the following hierarchy:

XAML Structure

Capturing BarChart Events

We setup the basic chart layout by adding an X-axis, a top title, and a bubble text.

// Handle events
chart1.BarClickHandler += new EventHandler<bareventargs>(BarClickHandler);

The BarChart event handler looks like this.

/// <summary />
/// This method is executed when any bar is clicked on the char.
/// </summary />
/// <param name="sender" />The sender of this event.</param />
/// <param name="e" />The BarChart event argument. This contains all the
/// information about the clicked bar.</param />
void BarClickHandler(object sender, BarEventArgs e)
{
    MessageBox.Show(e.BarObject.ToString());
}

In the above example, we are just displaying the information about the bar. The ToString() method of the BarObject returns a well-formatted string for debugging purposes. It looks like the figure shown below:

XAML Structure

Drawing the Chart

The main drawing activity happens in the Generate method.

  • 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.
  • Get the bar count.
  • Determine whether the chart is a single value or multi-valued (series).
  • Draw the X-axis based on the chart type.
  • Draw the Y-axis based on the chart type.
  • Draw the bar for each value in a series.
  • Calculate the next bar position.
  • Draw the X-axis.
  • Draw Y-axis.
  • Draw the legend.
/// <summary />
/// Creates the chart based on the datasource and the charttype.
/// </summary />
public void Generate()
{
    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 nodate found text.
        txtNoData.Visibility = Visibility.Hidden;

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

        // 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)
        {
            // Set the barwidth. This is required to adjust
            // the size based on available no. of bars.
            SetBarWidth(barCount);

            // Draw axis label based on charttype.
            if (Type == ChartType.VerticalBar)
                DrawXAxisLabel(row);
            else if (Type == ChartType.HorizontalBar)
                DrawYAxisLabel(row);

            
            // 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.
                switch (Type)
                {
                    case ChartType.VerticalBar:
                        DrawVerticalBar(isSeries, legendAdded, 
                                        row, ref currentSeries, valField);
                        break;
                    case ChartType.HorizontalBar:
                        DrawHorizontalBar(isSeries, legendAdded, 
                                          row, ref currentSeries, valField);
                        break;
                    default:
                        DrawVerticalBar(isSeries, legendAdded, row, 
                                        ref currentSeries, valField);
                        break;
                }
            }
            legendAdded = true;

            // Set up location for next bar in series.
            
            if (Type == ChartType.VerticalBar)
                left = left + spaceBetweenBars;
            else if (Type == ChartType.HorizontalBar)
                top = top + spaceBetweenBars + BarWidth + 10;
        }

        if (Type == ChartType.VerticalBar)
        {
            if ((left + BarWidth) > chartArea.Width)
                chartArea.Width = left + BarWidth + 20;
        }
        else if (Type == ChartType.HorizontalBar)
        {
            if ((top + BarWidth) > chartArea.Height)
                chartArea.Height = top + BarWidth + 20;

        }

        // Adjust chart element location after final chart rendering.
        AdjustChartElements();

        DrawXAxis();  // Draw x-axis
        DrawYAxis();  // Draw y-axis
        DrawLegend(); // Draw the legends
    }
}

Drawing the Bar

The DrawBar() function handles the rendering of each bar. The bar, in our case, is represented by a rectangle object. The following diagram shows the basic task we have to do to draw a simple bar:

/// <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 postion for subsequent bars.
    if (isSeries)
        left = left + rect.Width;
    else
        left = left + BarWidth + spaceBetweenBars;

    // Increment the series
    currentSeries++;  
}

Drawing the X-Axis

Setting up the C-axis is shown in the following figure:

XAML Structure

/// <summary />
/// Draws xAxis. For vertical bar chart it's as simple as drawing a line
/// beginning from top to bottom. For horizontal bar some calculations are
/// involved as we need to position the marker correctly along with display
/// value.
/// </summary />
private void DrawXAxis()
{
    // Draw axis
    if (Type == ChartType.VerticalBar)
    {
        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);
    }
    else if (Type == ChartType.HorizontalBar)
    {
        Line xaxis = new Line();
        xaxis.X1 = leftMargin;
        xaxis.Y1 = top;
        xaxis.X2 = this.chartArea.Width;
        xaxis.Y2 = top;

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


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

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

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

        // get the scale based on the current max value
        // and other chart element area adjustments.
        scale = (((float)xMarkerValue * 100 / maxData.Value)) *
            (chartArea.Width - leftMargin - rightMargin) / 100;

        double x1 = chartArea.Width - rightMargin;

        double xAxisValue = 0;

        x1 = leftMargin;
        for (double i = 0; i <= scaleFactor; i++)
        {
            // Add x-axis marker line chart.
            Line marker = AddMarkerLineToChart(x1);

            // Add the y-marker to the chart.
            AddMarkerTextToChart(x1 - scale, xAxisValue);

            // Adjust the top location for next marker.
            x1 += scale;

            // Increment the y-marker value.
            xAxisValue += xMarkerValue;
        }
    }
}

Drawing the Y-Axis

Setting up the Y-axis is shown in the following figure:

XAML Structure

/// <summary />
/// Draws YAxis. For horizantal chart it's as simple as drawing a line
/// beginning from top to bottom. For vertical bar some calculations are
/// involved as we need to position the marker correctly along with display
/// value.
/// </summary />
private void DrawYAxis()
{
    if (Type == ChartType.VerticalBar)
    {
        Line yaxis = new Line();
        yaxis.X1 = leftMargin;
        yaxis.Y1 = 0;
        yaxis.X2 = leftMargin;
        yaxis.Y2 = this.Height - bottomMargin;
        yaxis.Stroke = new SolidColorBrush(Colors.DarkBlue);
        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;
        }
    }
    else if (Type == ChartType.HorizontalBar)
    {
        Line yaxis = new Line();
        yaxis.X1 = leftMargin;
        yaxis.Y1 = 0;
        yaxis.X2 = leftMargin;
        yaxis.Y2 = top;
        yaxis.Stroke = new SolidColorBrush(Colors.DarkBlue);
        chartArea.Children.Add(yaxis);
    }
}

Drawing the Legend

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

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

    chartLegendArea.Visibility = Visibility.Visible;

    double x1 = 5;
    double y1 = 5;
    double legendWidth = 20;
    TextBlock tb;

    // Draw all legends
    foreach (Legend legend in legends)
    {
        Line legendShape = new Line();
      Size size = new Size(0, 0);
        
        // Calculate the legend width.
        for (int i = 0; i < legends.Count; i++)
        {
            tb = new TextBlock();
            tb.Text = legends[i].LegendText;
            tb.Measure(new Size(Double.PositiveInfinity, 
                                Double.PositiveInfinity));
            size = tb.DesiredSize;
            if (legendWidth < size.Width)
                legendWidth = size.Width;
        }

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

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

        legendShape.X1 = x1;
        legendShape.Y1 = y1;
        legendShape.X2 = x1 + legendWidth;
        legendShape.Y2 = y1;

        chartLegendArea.Children.Add(legendShape);

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

        chartLegendArea.Children.Add(txtLegend);
        
        Canvas.SetTop(txtLegend, y1 - size.Height /2);
        Canvas.SetLeft(txtLegend, legendShape.X1 + legendShape.X2 + 5);

        y1 += 15;
    }
}

Drawing the X-Axis Label

/// <summary />
/// Draw the x-axis label.
/// </summary />
/// <param name="row" />The bar data row.</param />
private void DrawXAxisLabel(DataRow row)
{
    // Setup XAxis label
    TextBlock markText = new TextBlock();
    markText.Text = row[XAxisField].ToString();
    markText.Width = 80;
    markText.HorizontalAlignment = HorizontalAlignment.Stretch;

    markText.Foreground = TextColor;
    markText.TextAlignment = TextAlignment.Center;
    markText.FontSize = 8;

    markText.MouseEnter += new MouseEventHandler(XText_MouseEnter);
    markText.MouseLeave += new MouseEventHandler(XText_MouseLeave);

    if (SmartAxisLabel)
    {
        Transform st = new SkewTransform(0, 20);
        markText.RenderTransform = st;
    }

    chartArea.Children.Add(markText);
    Canvas.SetTop(markText, this.Height - bottomMargin);  // adjust y location
    Canvas.SetLeft(markText, left + leftMargin / 2);
}

Drawing the Y-Axis Label

/// <summary />
/// Draw the y-axis label.
/// </summary />
/// <param name="row" />The bar data row.</param />
private void DrawYAxisLabel(DataRow row)
{
    TextBlock markText = new TextBlock();
    markText.Text = row[XAxisField].ToString();
    markText.Width = 80;
    markText.HorizontalAlignment = HorizontalAlignment.Stretch;
    markText.Foreground = TextColor;
    markText.TextAlignment = TextAlignment.Center;
    markText.FontSize = 8;

    markText.MouseEnter += new MouseEventHandler(XText_MouseEnter);
    markText.MouseLeave += new MouseEventHandler(XText_MouseLeave);

    if (SmartAxisLabel)
    {
        Transform st = new SkewTransform(0, 20);
        markText.RenderTransform = st;
    }

    chartArea.Children.Add(markText);
    // adjust y location
    Canvas.SetTop(markText, (top + (BarWidth * ValueField.Count)/2));
    Canvas.SetLeft(markText, left-leftMargin+10);
}

Feedback

I think the code is getting improved based on your feedback, so please provide constructive criticism as this will help me improve my WPF skills and I may be able to write better articles.

Points of Interest

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

  • Understanding plotting simple vertical and horizontal bar graphs.
  • Capture events on the bar. This could be used for the drill down feature.
  • Dynamically adjusting control size based on the available area.

Development Ideas

Highlighted below are some of the objectives which I would like to tackle in the next article. This is still pending from the first release, but hopefully, I will cover this over a period of time.

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

These objectives may get reshuffled over a period. New objectives may be added or deleted depending on the needs.

Credits

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

History

  • October 1, 2008 - Refactored and released with horizontal charting capability.

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