Click here to Skip to main content
15,886,362 members
Articles / Desktop Programming / WPF

Simple WPF Bar Chart Control

Rate me:
Please Sign up or sign in to vote.
4.81/5 (23 votes)
3 Oct 2008CPOL4 min read 127.8K   4K   58  
This article presents step-by-step instructions on how to create a simple bar chart using WPF.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Data;
using System.Windows.Media.Effects;
using System.Text.RegularExpressions;

namespace SimpleChart.Charts
{
    /// <summary>
    /// Interaction logic for BarChart.xaml
    /// </summary>
    public partial class BarChart : UserControl
    {
        private string xAxisText;
    
        private double horizontalGridLineThickness;
        
        private double defaultXYalueFontSize = 8;
        private double defaultYValueFontSize = 7;
        double? maxData;
        
        double left = 5;
        double top;

        double topMargin = 40;
        double leftMargin = 40;
        double bottomMargin = 100;

        double spaceBetweenBars = 8;

        private DataRow barRow;  // to hold current row 

        Brush prevBrush;

        Brush legendTextColor ;

        TextBlock txtXAxis;
        TextBlock txtTopTitle;
        TextBlock bubbleText;

        Color gridLineColor = Colors.LightGray;

        List<Legend> legends = new List<Legend>();

        public event EventHandler<BarEventArgs> BarClickHandler;

        public bool ShowValueOnBar { get; set; }
        public bool SmartAxisLabel { get; set; }

        public List<Legend> Legends
        {
            get { return legends; }
        }

        public double BarWidth { get; set; }
        public string Title { get; set; }
        public string ToolTipText  { get; set; }
        public string XAxisField { get; set; }
        public bool EnableZooming { get; set; }

        public string QueryParam { get; set; }
        
        public Color GridLineColor { get; set; }
        
        public string XAxisText
        {
            get { return xAxisText; }
            set 
            { 
                xAxisText = value;
                txtXAxis.Text = value;
            }
        }


        public List<string> ValueField  { get; set; }
        
        public Brush TextColor
        {
            get
            {
                return bubbleText.Foreground;
            }
            set
            {
                bubbleText.Foreground = value;
                txtTopTitle.Foreground = value;
                txtXAxis.Foreground = value;
            }
        }

        public double GridLineHorizontalThickness { get; set; }
        
        public bool ShowHorizontalGridLine { get; set; }
        
        public Brush BackGroundColor
        {
            get { return this.Background; }
            set
            {
                chartArea.Background = value;
                this.Background = value;
            }
        }

        public Brush LegendTextColor { get; set; }
        
             
        public DataSet DataSource { get; set; }
        
        public BarChart()
        {
            InitializeComponent();

           

            ValueField = new List<string>();
            InitChartControls();
            BarWidth = 60;
            horizontalGridLineThickness = 0.3;

            legendTextColor = new SolidColorBrush(Parser.GetDarkerColor(Colors.Yellow, 10));


            GradientStopCollection gsc = new GradientStopCollection(2);
            gsc.Add(new GradientStop(Colors.Black, 1));
            gsc.Add(new GradientStop(Colors.Gray, 0));

            chartArea.Background = new LinearGradientBrush(gsc, 90);

        }

        /// <summary>
        /// Get max value data element.
        /// </summary>
        /// <param name="dt"></param>
        /// <returns></returns>
        double? GetMax(DataTable dt)
        {
            double? max = 0;
            double? tmp = 0;


            foreach (string valField in ValueField)
            {
                foreach (DataRow r in dt.Rows)
                {
                    if (!r.Table.Columns.Contains(valField))
                        continue;
                    if (r[valField] != DBNull.Value)
                    {
                        tmp = Convert.ToDouble(r[valField]);
                        
                        if (tmp > max)
                            max = tmp;
                    }
                }
            }

            max = (max != null) ? max : 0;

            return max;
        }

        /// <summary>
        /// Reset the value field.
        /// </summary>
        public void Reset()
        {
            ValueField.Clear();
        }

        /// <summary>
        /// Creates the chart based on the datasource.
        /// </summary>
        public void Generate()
        {
            try
            {
                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 accomdodate 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();
                }
            }
            catch(Exception ex)
            {
                // TODO: Finalize exception handling strategy.
                MessageBox.Show(ex.Message);
            }
        }

        /// <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++;  
        }

        /// <summary>
        /// Setup bar events.
        /// </summary>
        /// <param name="rect"></param>
        private void SetBarEvents(Rectangle rect)
        {
            rect.MouseLeftButtonUp += new MouseButtonEventHandler(Bar_MouseLeftButtonUp);
            rect.MouseEnter += new MouseEventHandler(Bar_MouseEnter);
            rect.MouseLeave += new MouseEventHandler(Bar_MouseLeave);
        }

        /// <summary>
        /// Setup bar attributes.
        /// </summary>
        /// <param name="currentSeries"></param>
        /// <param name="calValue"></param>
        /// <param name="rect"></param>
        /// <returns></returns>
        private void SetBarAttributes(double? calValue, Rectangle rect)
        {
            rect.Width = BarWidth;
            if (calValue < 1)
                rect.Height = 1;
            else
                rect.Height = calValue.Value;

            rect.HorizontalAlignment = HorizontalAlignment.Left;
            rect.VerticalAlignment = VerticalAlignment.Center;
            rect.StrokeThickness = 1;
           
        }

        /// <summary>
        /// Display y-value on bar.
        /// </summary>
        /// <param name="val"></param>
        /// <param name="rect"></param>
        private void DisplayYValueOnBar(double val, Rectangle rect)
        {
            TextBlock yValue = new TextBlock();
            yValue.Text = val.ToString();
            yValue.Width = 80;
            yValue.Foreground = TextColor;
            yValue.HorizontalAlignment = HorizontalAlignment.Center;
            yValue.TextAlignment = TextAlignment.Center;
            yValue.FontSize = defaultYValueFontSize;

            yValue.MouseEnter += new MouseEventHandler(yValue_MouseEnter);
            yValue.MouseLeave += new MouseEventHandler(yValue_MouseLeave);
            chartArea.Children.Add(yValue);
            Canvas.SetTop(yValue, top - 10);
            Canvas.SetLeft(yValue, left + (rect.Width / 2));
        }

        private void SetBarWidth(int barCount)
        {
            BarWidth = (chartArea.Width - (spaceBetweenBars * ValueField.Count * barCount) -
                (leftMargin * 3)) / (barCount * ValueField.Count);

            // check min bar width
            if (BarWidth > 20 || BarWidth < 20)
                BarWidth = 20;
        }

        private void DrawXAxisLabel(DataRow row)
        {
            // Setup XAxis label
            TextBlock markText = new TextBlock();
            markText.Text = row[XAxisField].ToString();
            markText.Width = 80;
            //markText.TextTrimming = TextTrimming.CharacterEllipsis;
            markText.HorizontalAlignment = HorizontalAlignment.Stretch;

            markText.Foreground = TextColor;
            //markText.HorizontalAlignment = HorizontalAlignment.Center;
            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);
        }

        /// <summary>
        /// Prepares the chart for rendering.  Sets up control width and location.
        /// </summary>
        private void PrepareChartForRendering()
        {
            Canvas.SetTop(txtXAxis, this.Height - 20);
            Canvas.SetLeft(txtXAxis, leftMargin);

            txtTopTitle.Width = this.Width;
            txtTopTitle.FontSize = 14;
            txtTopTitle.Text = Title;
            txtTopTitle.TextAlignment = TextAlignment.Center;
            Canvas.SetTop(txtTopTitle, 0);
            Canvas.SetLeft(txtTopTitle, leftMargin);
        }

        /// <summary>
        /// Sets up the chart area with default values
        /// </summary>
        private void SetUpChartArea()
        {
            if (!EnableZooming)
            {
                zoomSlider.Visibility = Visibility.Hidden;
            }

            if (this.Height.ToString() == "NaN")
                this.Height = 450;

            chartArea.Height = this.Height;

            if (this.Width.ToString() == "NaN")
                this.Width = 800;

            chartArea.Width = this.Width;
        }

        /// <summary>
        /// Draws an empty chart.
        /// </summary>
        private void DrawEmptyChart()
        {
            txtNoData.Visibility = Visibility.Visible;
            Canvas.SetTop(txtNoData, chartArea.Height / 2);
            Canvas.SetLeft(txtNoData, chartArea.Width / 2);
        }

        void yValue_MouseLeave(object sender, MouseEventArgs e)
        {
            TextBlock tb = (sender as TextBlock);
            tb.FontSize = defaultXYalueFontSize;
        }

        void yValue_MouseEnter(object sender, MouseEventArgs e)
        {
            TextBlock tb = (sender as TextBlock);
            tb.FontSize = 10;
        }

        void XText_MouseLeave(object sender, MouseEventArgs e)
        {
            TextBlock tb = (sender as TextBlock);
            tb.FontSize = defaultXYalueFontSize;
        }

        void XText_MouseEnter(object sender, MouseEventArgs e)
        {
            TextBlock tb = (sender as TextBlock);
            tb.FontSize = 10;
        }

        
        /// <summary>
        /// Initialize chart controls.
        /// </summary>
        private void InitChartControls()
        {
            txtTopTitle = new TextBlock();
            txtXAxis = new TextBlock();
            bubbleText = new TextBlock();

            bubbleText.FontSize = 8;
            
            Transform tf = new ScaleTransform(1.5, 1.5, 12, 24);
            bubbleText.RenderTransform = tf;

         
        }

        /// <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);
        }


        /// <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;
            }
        }


        /// <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);

        }

        /// <summary>
        /// Draws YAxis.  Here we use the maxData vlaue 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;
            }
            
        }

        /// <summary>
        /// Add the marker line to chart.
        /// </summary>
        /// <param name="top">The top location where the marker is to be placed.</param>
        /// <returns>The marker line.  This is used for drawing the horizontal grid line.</returns>
        private Line AddMarkerLineToChart(double top)
        {
            Line marker = new Line();
            marker.X1 = leftMargin - 4;
            marker.Y1 = top;
            marker.X2 = marker.X1 + 4;
            marker.Y2 = top;
            marker.Stroke = new SolidColorBrush(Colors.Red);
            chartArea.Children.Add(marker);
            return marker;
        }

        /// <summary>
        /// Add marker text to chart on yaxis.
        /// </summary>
        /// <param name="top">The top location.</param>
        /// <param name="markerTextValue">The marker text value.</param>
        private void AddMarkerTextToChart(double top, double markerTextValue)
        {
            TextBlock markText = new TextBlock();
            markText.Text = markerTextValue.ToString();
            markText.Width = 30;
            markText.FontSize = 7;
            markText.Foreground = TextColor;
            markText.HorizontalAlignment = HorizontalAlignment.Right;
            markText.TextAlignment = TextAlignment.Right;
            chartArea.Children.Add(markText);

            Canvas.SetTop(markText, top - 10);        // adjust y location
            Canvas.SetLeft(markText, leftMargin - 40);
        }

        /// <summary>
        /// Draw horizontal Grid, if ShowHorizontalGridLine property is set.
        /// </summary>
        /// <param name="x1">starting left postion</param>
        /// <param name="y1">starting top postion</param>
        private void DrawHorizontalGrid(double x1, double y1)
        {
            if (!ShowHorizontalGridLine)
                return;

            Line gridLine = new Line();
            gridLine.X1 = x1;
            gridLine.Y1 = y1;
            gridLine.X2 = chartArea.Width;
            gridLine.Y2 = y1;

            gridLine.StrokeThickness = horizontalGridLineThickness;

            gridLine.Stroke = new SolidColorBrush(GridLineColor);
            
            chartArea.Children.Add(gridLine);

        }

        void Bar_MouseLeave(object sender, MouseEventArgs e)
        {
            Rectangle rect = (sender as Rectangle);
            rect.Fill = prevBrush;
            prevBrush = null;
            
        }

        void Bar_MouseEnter(object sender, MouseEventArgs e)
        {
            Rectangle rect = (sender as Rectangle);
            prevBrush = rect.Fill;

            rect.Fill = new SolidColorBrush(Colors.LightGreen);

            Bar b = rect.Tag as Bar;
            ToolTip tip = new ToolTip();

            barRow = b.BarRow;
            tip.Content = MatchToolTipTemplate(b.ValueField); 
            
            rect.ToolTip = tip;

        }

        /// <summary>
        /// Match tooltip template and replace fields.  Need help in improving this.
        /// Searches for a tokein in {} and replaces with the actual value from DataSource.
        /// Supports only single token replacement.  Need to add support for multiple token replacement.
        /// </summary>
        /// <returns></returns>

        public string MatchToolTipTemplate(string valueField)
        {
            //string matchExpression = @"{\w+}";
            //string matchExpression = @"{field}";
            //MatchEvaluator matchField = MatchEvaluatorField;
            
            return (ToolTipText.Replace("{field}", GetResolvedTemplateValue(valueField)));

            //return Regex.Replace(ToolTipText, matchExpression, matchField);
        }

        private string GetResolvedTemplateValue(string valueField)
        {
            string newText = "" ;
            try
            {
                newText = barRow[valueField].ToString();
            }
            catch { }
            return newText;
        }


        [Obsolete]
        private string MatchEvaluatorField(Match m)
        {
            string newText = m.Value.Replace('{',' ');
            newText = newText.Replace('}', ' ');
            try
            {
                newText = barRow[newText.Trim()].ToString();
            }
            catch { }
            return newText;
        }


        void Bar_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            Rectangle rect = (sender as Rectangle);
            Bar b = rect.Tag as Bar;
            
            if (BarClickHandler != null)
                BarClickHandler(this, new BarEventArgs(b));

        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Founder Algorisys Technologies Pvt. Ltd.
India India
Co Founder at Algorisys Technologies Pvt. Ltd.

http://algorisys.com/
https://teachyourselfcoding.com/ (free early access)
https://www.youtube.com/user/tekacademylabs/

Comments and Discussions