Click here to Skip to main content
16,018,653 members
Articles / Desktop Programming / WPF

Simple WPF Bar Graph Control

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
1 Apr 2016CPOL1 min read 14K   370   10  
This article presents a simple bar graph using an ItemsControl with a custom DataTemplate

Introduction

I needed a very simple bar graph to display a Histogram. Did not really want to add any specialized library since I had been keeping the project I was working on simple, with minimum in the way of third party libraries.

Code

The heart of this control is the DataTemplate for the column:

XML
<DataTemplate x:Key="HistogramColumnDataTemplate">
 <Grid>
  <Grid.Width>
   <MultiBinding Converter="{converters:DoubleDivideIntConverter}">
    <Binding Path="ActualWidth"
             RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
    <Binding Path="ItemsSource.Count"
             RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
   </MultiBinding>
  </Grid.Width>
  <Grid.ColumnDefinitions>
   <ColumnDefinition Width="*" />
   <ColumnDefinition Width="2*" />
   <ColumnDefinition Width="*" />
  </Grid.ColumnDefinitions>
  <Grid.RowDefinitions>
   <RowDefinition Height="{Binding Value, Converter={converters:LengthPercentageConverter},
                 ConverterParameter=reverse}" />
   <RowDefinition Height="{Binding Value, Converter={converters:LengthPercentageConverter}}" />
   <RowDefinition Height="Auto" />
   <RowDefinition Height="Auto" />
  </Grid.RowDefinitions>
  <Border Grid.Row="1"
          Grid.Column="1"
          Background="Blue"
          BorderBrush="Black"
          BorderThickness="1,1,1,0" />
  <Border Grid.Row="2"
          Grid.Column="0"
          Grid.ColumnSpan="3"
          Height="1"
          HorizontalAlignment="Stretch"
          Background="Black" />
  <TextBlock Grid.Row="3"
             Grid.Column="0"
             Grid.ColumnSpan="3"
             HorizontalAlignment="Center"
             FontSize="24"
             Text="{Binding Key}"
             TextAlignment="Center" />
 </Grid>
</DataTemplate>

This DataTemplate assumes that the Binding will be to a KeyValuePair. I used this because it is a standard class, and does the job, as opposed to creating something custom. The TextBox in the DataTemplate at the end displays the value associated with the column data.

This is used with an ItemsControl with a StackPanel as the ItemsPanelTemplate:

XML
<ItemsControl Grid.Row="1"
              ItemTemplate="{StaticResource HistogramColumnDataTemplate}"
              ItemsSource="{Binding HistogramData}">
 <ItemsControl.ItemsPanel>
  <ItemsPanelTemplate>
   <StackPanel Height="{Binding ActualHeight,
                                RelativeSource={RelativeSource AncestorType=ItemsControl}}"
               Orientation="Horizontal" />
  </ItemsPanelTemplate>
 </ItemsControl.ItemsPanel>
</ItemsControl>

There are two converter classes used to support this control: LengthPercentageConverter and DoubleDivideIntConverter:

LengthPercentageConverter

This LengthPercentageConverter is an IValueConverter that returns a GridLength with the GridUnitType of Star. The Value in the GridUnit is the value into the converter, or one minus that value is the ConverterParmeter contains the string "reverse". This converter is used with ColumnDefinitions for the Grid used to create the rectangle for each graph bar.

C#
public sealed class LengthPercentageConverter : MarkupExtension, IValueConverter
{
 public LengthPercentageConverter() { }

 public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
 {
  bool reverse = parameter?.ToString() == "reverse";
  if (string.IsNullOrWhiteSpace(value?.ToString())) return new GridLength(.5, GridUnitType.Star);
  double convertedValue = System.Convert.ToDouble(value);
  if (convertedValue >= 1) return new GridLength(reverse ? 0 : 1, GridUnitType.Star);
  if (convertedValue <= 0) return new GridLength(reverse ? 1 : 0, GridUnitType.Star);
  var gridLength = new GridLength(reverse ? 1 - convertedValue : convertedValue, GridUnitType.Star);
  return gridLength;
 }

 public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
 {
  throw new NotImplementedException();
 }

 public override object ProvideValue(IServiceProvider serviceProvider) { return this; }
}

DoubleDivideIntConverter

The DoubleDivideIntConverter is an IMultiValueConverter that used to specify the Width of the columns. This is the ActualWidth of the control divided by the number of KeyValuePair instances in the collection bound to the ItemsSource.

C#
internal class DoubleDivideIntConverter : MarkupExtension, IMultiValueConverter
{
 public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
 {
  if (!(values[0] is double) || !(values[1] is int)) return null;
  var size = (double) values[0];
  var count = (int) values[1];
  return size / count;
 }

 public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
 {
  throw new NotImplementedException();
 }

 public override object ProvideValue(IServiceProvider serviceProvider) { return this; }
}

The Sample

The ViewModel for this sample is very simple:

public class ViewModel
{
 public ViewModel()
 {
  HistogramData = new[] { new KeyValuePair<string, double>("25.0", .15),
       new KeyValuePair<string, double>("25.5", .20),
       new KeyValuePair<string, double>("26.0", .40),
       new KeyValuePair<string, double>("26.5", .15),
       new KeyValuePair<string, double>("27.0", .10) };
 }
 public IEnumerable<KeyValuePair<string, double>> HistogramData { get; set; }
 public double Maximum => .6;
}

Variation

Obviouslly this implementation leaves a lot to be desired given that the scale is always 0 to 1.0, which will actually handle most the problem, but requires that the values used in the ItemsSource be adjusted. A slight variation is to add a scale Maximum and Minimum. This will require that the LengthPercentageConverter be converted to a type of IMultiValueConverter, and a new DataTemplate.

The IMultiValueConverter:

C#
 public sealed class LengthPercentageConverter : MarkupExtension, IMultiValueConverter
{
  public LengthPercentageConverter() { }
  public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
  {
   bool reverse = parameter?.ToString() == "reverse";
   if (string.IsNullOrWhiteSpace(values[0].ToString())) return new GridLength(.5, GridUnitType.Star);
   if (values.Length == 1 || values[1] == DependencyProperty.UnsetValue) return Convert(values[0], 
        targetType, parameter, culture);
   if (string.IsNullOrWhiteSpace(values[1].ToString())) return new GridLength(.5, GridUnitType.Star);
   double convertedMin = 0;
   double convertedMax = System.Convert.ToDouble(values[1]);
   if (values.Length > 2 && values[2] != DependencyProperty.UnsetValue)
   {
    convertedMin = convertedMax;
    convertedMax = System.Convert.ToSingle(values[2]);
   }
   Single convertedValue = System.Convert.ToSingle(values[0]);
   if (convertedValue >= convertedMax) return new GridLength(reverse ? 0 
      : convertedMax - convertedMin, GridUnitType.Star);
   if (convertedValue <= convertedMin) return new GridLength(reverse ? convertedMax - convertedMin 
      : 0, GridUnitType.Star);
   var gridLength = new GridLength(reverse ? convertedMax - convertedMin - convertedValue 
      : convertedValue - convertedMin, GridUnitType.Star);
   return gridLength;
  }

  public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
  {
   throw new NotImplementedException();
  }

  public override object ProvideValue(IServiceProvider serviceProvider) { return this; }
 }

And the DataTemplate:

XML
 <DataTemplate x:Key="HistogramColumnDataTemplateWithMaximum">
 <Grid>
  <Grid.Width>
   <MultiBinding Converter="{local:DoubleDivideIntConverter}">
    <Binding Path="ActualWidth"
             RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
    <Binding Path="ItemsSource.Count"
             RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
   </MultiBinding>
  </Grid.Width>
  <Grid.ColumnDefinitions>
   <ColumnDefinition Width="*" />
   <ColumnDefinition Width="2*" />
   <ColumnDefinition Width="*" />
  </Grid.ColumnDefinitions>
  <Grid.RowDefinitions>
   <RowDefinition>
    <RowDefinition.Height>
     <MultiBinding Converter="{local:LengthPercentageConverter}"
                   ConverterParameter="reverse">
      <Binding Path="Value" />
      <Binding Path="DataContext.Maximum"
               RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
     </MultiBinding>
    </RowDefinition.Height>
   </RowDefinition>
   <RowDefinition Height="{Binding Value, Converter={local:LengthPercentageConverter}}" />
   <RowDefinition Height="Auto" />
   <RowDefinition Height="Auto" />
  </Grid.RowDefinitions>
  <Border Grid.Row="1"
          Grid.Column="1"
          Background="Blue"
          BorderBrush="Black"
          BorderThickness="1,1,1,0" />
  <Border Grid.Row="2"
          Grid.Column="0"
          Grid.ColumnSpan="3"
          Height="1"
          HorizontalAlignment="Stretch"
          Background="Black" />
  <TextBlock Grid.Row="3"
             Grid.Column="0"
             Grid.ColumnSpan="3"
             HorizontalAlignment="Center"
             FontSize="24"
             Text="{Binding Key}"
             TextAlignment="Center" />
 </Grid>
</DataTemplate>

History

04/01/2016: Initial version

License

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


Written By
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
-- There are no messages in this forum --