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
Horizontal Bar Chart
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.
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.
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>
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
:
chart1.Type = BarChart.ChartType.HorizontalBar;
To create a VerticalBar
chart, use the snippet below..
chart2.Type = BarChart.ChartType.VerticalBar;
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:
We setup the basic chart layout by adding an X-axis, a top title, and a bubble text.
chart1.BarClickHandler += new EventHandler<bareventargs>(BarClickHandler);
The BarChart
event handler looks like this.
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:
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.
public void Generate()
{
legends.Clear();
chartArea.Children.Clear();
AddChartControlsToChart();
SetUpChartArea();
DataTable dt = (DataSource as DataSet).Tables[0];
if (null != dt)
{
if (dt.Rows.Count == 0)
{
DrawEmptyChart();
return;
}
txtNoData.Visibility = Visibility.Hidden;
maxData = GetMax(dt);
int barCount = dt.Rows.Count;
bool isSeries = ValueField.Count > 1;
bool legendAdded = false;
foreach (DataRow row in dt.Rows)
{
SetBarWidth(barCount);
if (Type == ChartType.VerticalBar)
DrawXAxisLabel(row);
else if (Type == ChartType.HorizontalBar)
DrawYAxisLabel(row);
int currentSeries = 0;
foreach (string valField in ValueField)
{
if (null == valField)
continue;
if (!row.Table.Columns.Contains(valField))
continue;
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;
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;
}
AdjustChartElements();
DrawXAxis();
DrawYAxis();
DrawLegend();
}
}
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:
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]);
double? calValue = (((float)val * 100 / maxData)) *
(chartArea.Height - bottomMargin - topMargin) / 100;
Rectangle rect = new Rectangle();
SetBarAttributes(calValue, rect);
Color stroke = Helper.GetDarkColorByIndex(currentSeries);
rect.Fill = new SolidColorBrush(stroke);
SetBarEvents(rect);
if (isSeries && !legendAdded)
{
legends.Add(new Legend(stroke, ValueField[currentSeries]));
}
top = (chartArea.Height - bottomMargin) - rect.Height;
Canvas.SetTop(rect, top);
Canvas.SetLeft(rect, left + leftMargin);
chartArea.Children.Add(rect);
if (ShowValueOnBar)
{
DisplayYValueOnBar(val, rect);
}
rect.Tag = new Bar(val, row, valField);
if (isSeries)
left = left + rect.Width;
else
left = left + BarWidth + spaceBetweenBars;
currentSeries++;
}
Setting up the C-axis is shown in the following figure:
private void DrawXAxis()
{
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);
double scaleFactor = 10;
double xMarkerValue = Math.Ceiling(maxData.Value / scaleFactor);
double scale = 5;
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++)
{
Line marker = AddMarkerLineToChart(x1);
AddMarkerTextToChart(x1 - scale, xAxisValue);
x1 += scale;
xAxisValue += xMarkerValue;
}
}
}
Setting up the Y-axis is shown in the following figure:
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);
double scaleFactor = 10;
double yMarkerValue = Math.Ceiling(maxData.Value / scaleFactor);
double scale = 5;
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++)
{
Line marker = AddMarkerLineToChart(y1);
DrawHorizontalGrid(marker.X1, y1);
AddMarkerTextToChart(y1, yAxisValue);
y1 -= scale;
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 a legend is pretty straightforward. Loop through the legends
collection, and draw a line and text at the appropriate locations with the specified colors.
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;
foreach (Legend legend in legends)
{
Line legendShape = new Line();
Size size = new Size(0, 0);
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;
}
}
private void DrawXAxisLabel(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);
Canvas.SetTop(markText, this.Height - bottomMargin);
Canvas.SetLeft(markText, left + leftMargin / 2);
}
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);
Canvas.SetTop(markText, (top + (BarWidth * ValueField.Count)/2));
Canvas.SetLeft(markText, left-leftMargin+10);
}
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.
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.
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.
All credits to CodeProject WPF article writers as I am learning most from their article.
- October 1, 2008 - Refactored and released with horizontal charting capability.