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:
<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
:
<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.
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
.
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
:
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
:
<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
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.