in

Software FX Community

Discuss and find help for all Software FX products.

WPF Blog

May 2009 - Posts

  • Hiding series using the Legend Box

    If you have a chart with multiple series and need to provide UI to show/hide series, you can use styling on the legend box items to display a checkbox for each series.

    xmlns:cfxConverters="http://schemas.softwarefx.com/chartfx/wpf/80/converters"
    
    <DataTemplate x:Key="CheckLegend">
      <DataTemplate.Resources>
        <cfxConverters:VisibilityToBooleanConverter x:Key="VisibilityToBool"
    FalseVisibility="Hidden"/> </DataTemplate.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <CheckBox Margin="2,0" VerticalAlignment="Center"
    IsChecked="{Binding Path=Visibility,
    Converter={StaticResource VisibilityToBool}}" /> <Rectangle Stroke="{Binding Path=Stroke}" Fill="{Binding Path=Fill}"
    Grid.Column="1" Width="12" Height="12" VerticalAlignment="Center"
    Margin="2,0" /> <TextBlock FontFamily="{Binding Path=FontFamily}"
    FontSize="{Binding Path=FontSize}" Text="{Binding Path=Text}"
    Grid.Column="2" VerticalAlignment="Center" Margin="2,0" /> </Grid> </DataTemplate>

    Unfortunately templating items in the legend box has to be done in the code because of our API so we need to add the following line of code to our Window/Page Loaded event (we expect to support a XAML way to set these templates in future builds but you know what they say … shipping is also a feature)

    LegendItemAttributes itemAttr = chart1.LegendBox.ItemAttributes[chart1.Series];
    itemAttr.Template = (DataTemplate) FindResource("CheckLegend");

    CheckLegend1

    Where do all these bindings come from?

    Note that you are styling a logical item we create internally in code, this class has information about the specific series (Fill, Stroke, Text) as well as “global” information about the legend box (FontFamily, FontSize, etc.). This class also will pick up individual changes applied to a specific series in the legend box when using the ItemAttributes[collection, index] API.

    If you want to see which properties you can bind to you can use the following trick: setup one of your bindings with an invalid path, e.g. Stroke="{Binding Path=FooBar}" , debug your app and note the message in the Visual Studio output window that looks like this

    System.Windows.Data Error: 39 : BindingExpression path error: 'FooBar' property not found on 'object'
    ''LegendSeriesItem' (HashCode=45082239)'. BindingExpression:Path=FooBar;
    DataItem='LegendSeriesItem' (HashCode=45082239); target element is 'Rectangle' (Name='');
    target property is 'Stroke' (type 'Brush')

    This will give you the class name you are styling, in this case is called LegendSeriesItem. Then you can use ildasm or reflector to view all the properties exposed by this class.

    The VisibilityToBoolConverter exposed in the ChartFX.WPF.Converters namespace is used to convert between Visibility and booleans and also allows you to specify whether false should be translated as Hidden or Collapsed. A Hidden series will not be plotted but will still interact with the axis while a collapsed series will not interact with the axis. This means that if Series1 goes from 0 to 100 and Series2 goes from 0 to 20 and you collapse Series1, now the Y axis will rescale to show 0 to 20, if Series1 instead was Hidden, the axis would remain showing the 0 to 100 range.

    Fine tuning our template

    Now is time to make sure our template works in other situations, if you switch the gallery to line you will quicky notice that our legend box items will still show rectangles even though Chart FX would have used markers in the legend box by default. We need to then change the Rectangle in our template as follows

    xmlns:cfxControls="http://schemas.softwarefx.com/chartfx/wpf/80/controls"
    
    <
    cfxControls:MarkerLegendControl Content="{Binding Path=Self}"
    Grid.Column="1" Margin="2,0" />

    This control requires an extra namespace that you should add to the root of your XAML node. Now a line chart will look like this

    CheckLegend2

    If you hover your mouse over one of the lines you will notice that the checkbox and series text do not get dimmed, this can be solved by adding a trigger to our datatemplate

          <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding Path=Dimmed}">
              <DataTrigger.Value>
                <sys:Boolean>True</sys:Boolean>
              </DataTrigger.Value>
              <Setter Property="Opacity" Value="0.25"/>
            </DataTrigger>
          </DataTemplate.Triggers>
    

    Finally even though you might be using just a string to show what a series represents, Chart FX offers Content and ContentTemplate properties that allows you to provide visuals for your series, e.g.

      <cfx:Chart.Series>
        <cfx:SeriesAttributes Content="US">
          <cfx:SeriesAttributes.ContentTemplate>
            <DataTemplate>
              <StackPanel Orientation="Horizontal">
                <Image Source="pack://siteoforigin:,,,/Img/US.png" Width="16"/>
                <TextBlock Text="United States" Margin="4,0,0,0"/>
              </StackPanel>
            </DataTemplate>
          </cfx:SeriesAttributes.ContentTemplate>
        </cfx:SeriesAttributes>
        <cfx:SeriesAttributes Content="Canada">
          <cfx:SeriesAttributes.ContentTemplate>
            <DataTemplate>
              <StackPanel Orientation="Horizontal">
                <Image Source="pack://siteoforigin:,,,/Img/Canada.png" Width="16"/>
                <TextBlock Text="Canada" Margin="4,0,0,0"/>
              </StackPanel>
            </DataTemplate>
          </cfx:SeriesAttributes.ContentTemplate>
        </cfx:SeriesAttributes>
      </cfx:Chart.Series>
    

    In this case, our TextBlock object in the legend box template will clearly be insufficient so we have to replace it with a ContentControl as follows

    <Border Background="Transparent" Grid.Column="2"
    Margin="2,0" VerticalAlignment="Center"> <ContentControl IsHitTestVisible="False" Content="{Binding Path=Content}"
    ContentTemplate="{Binding Path=ContentTemplate}"
    Foreground="{Binding Path=Foreground}"
    FontFamily="{Binding Path=FontFamily}" FontSize="{Binding Path=FontSize}"
    FontStyle="{Binding Path=FontStyle}" FontWeight="{Binding Path=FontWeight}"/> </Border>

    The reason why we set IsHitTestVisible to false in the ContentControl and wrap it in a border is due to highlighting, you can verifiy that if you remove the property and the border and move the Grid.Column and Margin to the ContentControl, hovering your mouse over the flag will not cause a series highlight.

    Also important is the fact that we used ContentTemplate instead of setting up our visuals with the Content property. The reason for this is that we will try to use the Series.Content in several places, for example in the tooltips and a visual cannot be hosted in 2 places, by using ContentTemplate we are making sure that a copy of the visual is being created as needed.

    CheckLegend3

    JuanC

    The following APIs were not harmed during the production of this post

    LegendBox.ItemAttributes: To set properties for a collection of items (or specific item) in the legend box.
    VisibilityToBoolConverter: Converts from visibility to bool including support to specify which visibility should be used for false.
    MarkerLegendControl: To display a marker in the legend box adapting to the series gallery
    Series.Content and Series.ContentTemplate: To change series representation in LegendBox, Tooltips and DataView.

    Posted May 28 2009, 05:02 PM by JuanC with no comments
    Filed under: ,
  • Minimizing Ink

    Sometimes you need nice looking charts with lots of gradients and animations but sometimes what you really need is to maximize the space and show trends without too many visual artifacts, this is something that Edward Tufte refer to as “Minimizing Ink”, although WPF controls tend to be on the fancy side of the equation, you can actually achieve both with little effort.

    Imagine for example that you have a list of products and want to show a list of products including general information about the product (Product Name, Version, etc.) along with a trend of how these products are being downloaded.

    public class ProductInfo
    {
        public string Name { get; set; }
        public string Version { get; set; }
        public List<ProductDownloads> Downloads { get; set; }
        public string LatestRelease { get; set; } 
    } public class ProductDownloads { public double Count { get; set; } }

    For simplicity we will use a templated listbox to show multiple columns so our first approach to the problem will look like this

    <ListBox Grid.IsSharedSizeScope="true">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <Grid>
            <Grid.ColumnDefinitions>
              <ColumnDefinition Width="Auto" SharedSizeGroup="ColumnName"/>
              <ColumnDefinition Width="Auto" SharedSizeGroup="ColumnVersion"/>
              <ColumnDefinition Width="150" />
              <ColumnDefinition Width="Auto" SharedSizeGroup="ColumnRelease"/>
            </Grid.ColumnDefinitions>
            <TextBlock VerticalAlignment="Center" Grid.Column="0"
    Text="{Binding Path=Name}" Margin="4,0"/> <TextBlock VerticalAlignment="Center" Grid.Column="1"
    Text="{Binding Path=Version}" Margin="4,0"/> <cfx:Chart x:Name="chart1" Grid.Column="2"
    ItemsSource="{Binding Path=Downloads}"
    Gallery="Line" Margin="4,0"> <cfx:Chart.Series> <cfx:SeriesAttributes BindingPath="Count"/> </cfx:Chart.Series> <cfx:Chart.LegendBox> <cfx:LegendBox Visibility="Collapsed"/> </cfx:Chart.LegendBox> </cfx:Chart> <TextBlock VerticalAlignment="Center" Grid.Column="3"
    Text="{Binding Path=LatestRelease}" Margin="4,0"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox>

    MinimizeInk1

    Clearly not a good example of ink minimizing but we have to start somewhere, note that we are setting up all the bindings and we are also hiding the chart’s legend box, we are not specifying the size of the chart (because the desired size would be impossible to achieve with our default style).

    The listbox is populated with a list of ProductInfo so our datatemplate applies to each product, because of this, we can simply point the chart’s ItemsSource property to a Binding for the Downloads collection. Inside of the chart we want 1 data series using the Count property.

    Now that we got the basis of our sample out of the way we will focus on the chart customizations

    <cfx:Chart x:Name="chart1" Grid.Column="2" ItemsSource="{Binding Path=Downloads}"
    Gallery="Line" Margin="4,0" Height="20"> <cfx:Chart.Series> <cfx:SeriesAttributes BindingPath="Count" Stroke="Black" StrokeThickness="1"> <cfx:SeriesAttributes.Marker> <cfx:MarkerAttributes Visibility="Collapsed"/> </cfx:SeriesAttributes.Marker> </cfx:SeriesAttributes> </cfx:Chart.Series> <cfx:Chart.LegendBox> <cfx:LegendBox Visibility="Collapsed"/> </cfx:Chart.LegendBox> <cfx:Chart.AxisY> <cfx:Axis Visibility="Collapsed"/> </cfx:Chart.AxisY> <cfx:Chart.AxisX> <cfx:Axis Visibility="Collapsed"/> </cfx:Chart.AxisX> <cfx:Chart.PlotArea> <cfx:PlotAreaAttributes Margin="0" AxesStyle="None"
    Background="{x:Null}" Stroke="{x:Null}"/> </cfx:Chart.PlotArea> <cfx:Chart.Template> <ControlTemplate> <Border cfx:Chart.PanelName="Plot"/> </ControlTemplate> </cfx:Chart.Template> </cfx:Chart>

    In this step, we have done a series of simple changes such as hiding both axes and markers and also setting some brushes, note that our plotarea is normally surrounded by some margin but in such a small chart we do not need any margins (PlotArea.Margin). Because of all these changes we can now set the chart’s height to 20.

    We are also simplifying the chart template, a similar result could be accomplished by using one of our predefined styles (ChartFX.WPF.Motifs.Simple.Style) but this style still allows you to use a legend box or dataview so it has some unnecessary visuals. The template we are using only contains a border that will hold the plotarea, note that this border does not try to honor the chart’s background but this is also not needed.

    MinimizeInk2

    We can now focus on some fit and finish, note that by default a listbox will change the Foreground of the selected item to white but the chart is not honoring it. We will need a trigger in the DataTemplate to accomplish this.

    <DataTemplate.Triggers>
      <DataTrigger
    Binding="{Binding RelativeSource={RelativeSource FindAncestor,
    AncestorType={x:Type ListBoxItem}}, Path=IsSelected}"
    Value="True"> <Setter TargetName="series" Property="Stroke" Value="White" /> </DataTrigger> </DataTemplate.Triggers>

    Because the DataTemplate is actually applied to a ContentPresenter we need our trigger to find the first ancestor of type ListBoxItem as this class will actually hold the IsSelected

    MinimizeInk3

    Even though the charts are very small you can still hover over the line and you will get a tooltip, this can be disabled by setting the Series.Tooltips.IsEnabled to false. You could also modify the trigger if you wanted tooltips only on the selected chart.

        <cfx:SeriesAttributes.ToolTips>
          <cfx:ToolTipAttributes x:Name="tooltips" IsEnabled="False"/>
        </cfx:SeriesAttributes.ToolTips> 
      <DataTemplate.Triggers>
        <DataTrigger
    Binding="{Binding RelativeSource={RelativeSource FindAncestor,
    AncestorType={x:Type ListBoxItem}}, Path=IsSelected}"
    Value="True"> <Setter TargetName="series" Property="Stroke" Value="White" /> <Setter TargetName="tooltips" Property="IsEnabled" Value="True" /> </DataTrigger> </DataTemplate.Triggers>

    We hope to post more how-to's with a variety of chart scenarios using Chart FX for WPF, please feel free to use the comments if there is a particular chart you want to accomplish.

    JuanC

    Posted May 22 2009, 06:00 PM by JuanC with 4 comment(s)
    Filed under: ,
Copyright 2008 Software FX, Inc.