in

Software FX Community

Discuss and find help for all Software FX products.

WPF Blog

August 2009 - Posts

  • Minimizing Ink (Part 2)

    This post will add features to the sparkline chart built on our previous minimizing ink post and it is driven by comments and emails we have received about this topic.

    Q: One reader asks “The value being plotted will have a min and max range. I would like to draw a light gray rectangle in the back ground using these ranges and overlap the plot on top. If any of the values are out of range I can easily looking at the sparkline. do you have any idea how to do this?”

    A: There are two important issues in order to implement this feature, first our Y axis was not visible in order to minimize space but if we want to show this range using Axis sections we have to make sure the axis is visible without showing any labels or gridlines. The XAML required to hide the labels and gridlines looks like this

    <cfx:Chart.AxisY>
      <cfx:Axis Separation="0">
        <cfx:Axis.Labels>
          <cfx:AxisLabelAttributes Visibility="Collapsed"/>
        </cfx:Axis.Labels>
        <cfx:Axis.Grids>
          <cfx:Grids>
            <cfx:Grids.Major>
              <cfx:GridLine Visibility="Collapsed" TickMark="None"/>
            </cfx:Grids.Major>
            <cfx:Grids.Minor>
              <cfx:GridLine Visibility="Collapsed" TickMark="None"/>
            </cfx:Grids.Minor>
          </cfx:Grids>
        </cfx:Axis.Grids>
      </cfx:Axis>
    </cfx:Chart.AxisY>
    

    Hopefully how to hide gridlines and labels was intuitive but setting Separation to 0 is key to minimize the space we assign to the axis. Now we are ready to create an axis section, I will assume that the min and max range is calculated per product (per chart) so we will assume that our data layer class ProductInfo has 2 extra properties called MinDownloads and MaxDownloads.

        <cfx:Axis.Sections>
          <cfx:AxisSection From="{Binding Path=MinDownloads}"
    To="{Binding Path=MaxDownloads}" Background="#A0A0A0" /> </cfx:Axis.Sections>

    This results in a range shown per product, note that AxisSection.From and AxisSection.To were not Dependency Properties on previous builds so you will have to make sure you are using build 3516 or later in order to get the bindings to work.

    MinimizeInk4

    Q: An email asks the following “Is it possible to display a marker on the max value on each line?”

    A: In order to implement this we have added a MaximumValueCondition and MinimumValueCondition classes in build 3517 or later, this will allow you to use our conditional attributes as follows

      <cfx:Chart.ConditionalAttributes>
        <cfx:ConditionalAttributes>
          <cfx:ConditionalAttributes.Condition>
            <cfx:MaximumValueCondition/>
          </cfx:ConditionalAttributes.Condition>
          <cfx:ConditionalAttributes.Marker>
            <cfx:MarkerAttributes Visibility="Visible" Fill="Red"
    Size="6" Shape="Circle" /> </
    cfx:ConditionalAttributes.Marker> </cfx:ConditionalAttributes> </cfx:Chart.ConditionalAttributes>

    Now you can quickly see when the number of downloads for each product peaked. Note that our MaximumValueCondition class might flag more than 1 point if 2 or more points share the max value, also note that in a multi series chart we will default to calculate the max per series but you can change this behavior setting the PerSeries property to false.

    MinimizeInk5

    Finally we can use the same class but bind it to the X value if we want to display a point label showing the last value on each chart.

          <cfx:ConditionalAttributes>
            <cfx:ConditionalAttributes.Condition>
              <cfx:MaximumValueCondition BindingPath="X"/>
            </cfx:ConditionalAttributes.Condition>
            <cfx:ConditionalAttributes.PointLabels>
              <cfx:PointLabelAttributes Visibility="Visible" Offset="3,0"
    HorizontalAlignment="Right"
    VerticalAlignment="Center" FontSize="7"/> </cfx:ConditionalAttributes.PointLabels> </cfx:ConditionalAttributes>

    Note that in this case we are showing the point labels for the point that matches this condition and we are making sure the label is properly aligned to the right of the line.

    MinimizeInk6

    JuanC

    Posted Aug 19 2009, 04:50 PM by JuanC with no comments
    Filed under: ,
  • Comparing multiple variables using a pane matrix (Part 2 of 2)

    In part 1 we showed how to use the crosstab transform and the GridPanePanel to create a pane matrix where users can quickly compare multiple variables, in this post we will try to improve other aspects of the chart, one of the main differentiators WPF brings to the table is the use of data templates to include rich information about your data in any control. In our sample let’s assume that instead of sales per region, we are tracking sales per country, this means that our data layer could easily add a string property that returns a URI with the flag for such country. When crosstab returns the per-dimension information we keep track of the first element that belongs to each dimension, e.g.

    DataTemplate regionTemplate = (DataTemplate) FindResource("regionTemplate");
    
    foreach (CrosstabDimensionValue columnDimension in crosstab1.Dimensions[0]) {
        GridPanePanelColumnDefinition columnDefinition =
    new GridPanePanelColumnDefinition(); // columnDefinition.Header = columnDimension.Name; ContentControl contentControl = new ContentControl(); contentControl.Content = columnDimension.DataItem; contentControl.ContentTemplate = regionTemplate; columnDefinition.Header = contentControl; gridPanePanel.ColumnDefinitions.Add(columnDefinition); }
    <DataTemplate x:Key="regionTemplate">
      <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
        <Image Source="{Binding Path=Image}" Width="15" Height="20" Margin="0,0,4,0"/>
        <TextBlock Text="{Binding Path=Region}" VerticalAlignment="Center"/>
      </StackPanel>
    </DataTemplate>
    

    PaneMatrix6

    Note the images on top which will help when trying to locate a specific country, by hovering over a specific point I wanted to point out 2 details where the trick in which each series represents a combination of Region+Product+Year is leaking out. First note that the first line of the tooltip reads “Japan, ABC Widget, 2007” , also note that no other 2007 plot is highlighted even though we created a fake legend and made sure all lines that represent 2007 have the same color.

    To fix the first issue we will use the fact that crosstab will automatically assign to each series the first object that matched that series as the Series.Content so all we need to do is provide a template and use it as the Series.ContentTemplate

    <DataTemplate x:Key="seriesTemplate">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="Auto" />
          <ColumnDefinition Width="4" />
          <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto" />
          <RowDefinition Height="Auto" />
          <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock Text="Region:" Grid.Row="0" Grid.Column="0"/>
        <TextBlock Text="{Binding Path=Region}" Grid.Row="0" Grid.Column="2"/>
        <TextBlock Text="Product:" Grid.Row="1" Grid.Column="0"/>
        <TextBlock Text="{Binding Path=Product}" Grid.Row="1" Grid.Column="2"/>
        <TextBlock Text="Year:" Grid.Row="2" Grid.Column="0"/>
        <TextBlock Text="{Binding Path=Year}" Grid.Row="2" Grid.Column="2"/>
      </Grid>
    </DataTemplate>
      <cfx:Chart.AllSeries>
        <cfx:AllSeriesAttributes ContentTemplate="{StaticResource seriesTemplate}"/>
      </cfx:Chart.AllSeries>
    PaneMatrix7 

    I combined 2 screenshots so that you can see that our solution works well both when you hover over a point and over the line connecting 2 points, there is an alternate solution where you can set the AllSeries.ToolTips Template and ConnectedTemplate independently, this would give finer control over the information shown in both cases.

    To improve highlighting  we will use a new feature introduced in build 3515 or later. Internally we use an interface called IHighlightResolver to allow different implementations of what should be dimmed/highlighted, although this interface has always been public we did not have an easy entry point where you could plug your own highlighter. Now there is an event in Chart.Highlight called CreatingResolver which allows just that, to make this scenario even easier we also created a CrosstabDimensionResolver in the ChartFX.WPF.Data assembly that knows about how crosstabs joins multiple series.

    private void PageLoaded (object sender, EventArgs e)
    {
        chart1.DataBound += new EventHandler(OnChartDataBound);
        chart1.Highlight.CreatingResolver +=
    new HighlightResolverEventHandler(OnCreatingResolver); } private void OnCreatingResolver (object sender, HighlightingEventArgs args) { args.Resolver = new CrosstabDimensionResolver(args, crosstab1); }
    PaneMatrix8

     

    Now highlight works and you can see all the 2007-related series, now let’s assume that your data really contains several sales per month for each region+product, crosstab will automatically accumulate on a per-month basis (because you are creating a row per month) but these details will be hidden on the chart. To show these details (you could call this drill-down) we will take the following steps

    • Crosstab.AggregateElements: When this property is true, crosstab will group all data items that form a specific series/row.
    • Chart.Selection: We need to tell Chart FX that we want to keep track of the selected item
    • Chart.ClickCommand: Allows us to specify what will happen when the user clicks a marker
    • Chart.InfoWinfow: We will show this tool and it will be templated to show the details of the selected item
    • AllSeries.Marker.Template: Provide a new template that will draw a rounded rectangle around the selected marker.
    <cfx:Chart Template="{StaticResource chartTemplate}"
    ClickCommand="{x:Static cfx:Chart.SelectCommand}"> <cfx:Chart.AllSeries> <cfx:AllSeriesAttributes> <cfx:AllSeriesAttributes.Marker> <cfx:MarkerAttributes Template="{StaticResource markerMarbleSelection}"/> </cfx:AllSeriesAttributes.Marker> </cfx:AllSeriesAttributes> </cfx:Chart.AllSeries> <cfx:Chart.InfoWindow> <cfx:InfoWindow Background="Transparent" Visibility="Visible"
    ContentTemplate="{StaticResource infowindowTemplate}"/> </cfx:Chart.InfoWindow> <cfx:Chart.Selection> <cfx:SelectionAttributes IsEnabled="true"/> </cfx:Chart.Selection> </cfx:Chart>

    I omitted the templates for both the marker and the infowindow to keep the length of this post under control, you can download the attachment that contains all the necessary XAML and code included as an attachment.

    PaneMatrix9 PaneMatrix10

    We hope this post will allow you to see the power that Chart FX for WPF exposes to implement BI solutions in your WPF application.

    JuanC

  • Plotting Geographical Data as a Contour

    Note: This post makes use of the XYZExtension, which at the time the post was written was not available on the public version of Chart FX for WPF. If you are interested in using the extension before its release with the next Service Pack you may download the hotfix at http://support.softwarefx.com/cfxwpf/update/hotfix/hotfix.htm.

    Chart FX for WPF is about to introduce a brand new feature: the XYZExtension. This extension will allow us to add and control a third axis (Z axis) on a 3D chart. For the first time it will be possible to plot X, Y and Z values to a Chart FX chart. Right out of the gate there are two different galleries that can use this extension: SurfaceXYZ and ScatterXYZ. Multiple series of each or even combinations are also possible.

    One of the cool things I was able to accomplish with the new SurfaceXYZ chart was create geographically accurate surface with data from different cities. I can see multiple uses for a chart like this, from temperature plotting, revenue per county or even sales per store in one state.

    Suppose for the sake of this example that we need to plot the customer satisfaction rating on a little more than 800 stores across Florida. Plotting it on a bar chart will create more than 800 bars. And even if we order the cities alphabetically, while locating the score of a specific store should be easy, it is not so simple to understand which areas of the state are doing better than the others. Plotting it on a surface, on the other hand, will group all the stores by proximity allowing us to locate the best and worse regions. Particularly if we can do so over a map of the state. Here is what our final surface chart should look like.

    Untitled5

    Looking at that map it is very easy to spot that the cities stores around Jacksonville and the west coast are doing better than the ones on the east coast of Florida. Let’s look at what was necessary to obtain that great chart, shall we?

    First thing we need is a map. I have selected a Mercator projection of the state, with no county lines. If you have not noticed yet, we are laying the map on top of our surface. That is done because the triangulation used for our surface will interpolate all the data passed to our chart, creating a false impression that data was collected from stores located in the Gulf of Mexico. To take care of that, we lay a map over the surface where the state itself is transparent. That way the non-transparent ocean will cover the interpolated data that does not make sense. Here is what our map looks like without our surface.

    image5

    Now that we have selected a map, let’s look at a slice of our data.

    <Store CITY="MIAMI" LATITUDE="25.64" LONGITUDE="-80.32" SCORE="91" />
    <Store CITY="NORTH MIAMI BEAC" LATITUDE="25.94" LONGITUDE="-80.14" SCORE="89" />
    <Store CITY="NORTH MIAMI" LATITUDE="25.89" LONGITUDE="-80.18" SCORE="91" />
    <Store CITY="NORTH MIAMI BEAC" LATITUDE="25.93" LONGITUDE="-80.18" SCORE="91" />
    <Store CITY="OLYMPIA HEIGHTS" LATITUDE="25.74" LONGITUDE="-80.36" SCORE="92" />
    <Store CITY="MIAMI SPRINGS" LATITUDE="25.82" LONGITUDE="-80.30" SCORE="92" />

    We have the latitude and longitude data for the store. While that makes it perfect to locate it via GPS, it does not make for a surface friendly data. So we are going to have to convert it to pixel values on our selected map. Since this is a Mercator projection, we use the formulas bellow to convert the data (where φ is the latitude and λ is the longitude).

    image6 

    To keep this a short post (or try to) I won’t post the full code for the conversion. But I will make it available as a sample on the next Service Pack (or you can ask support for it, at support [at] softwarefx [dot] com).

    Once we have our data formatted in a way we can use, let’s pass it to the chart and see what we get.

    SurfaceXYZ surfaceXYZ = new SurfaceXYZ();
    surfaceXYZ.ShowPointsGridlines = false;
    surfaceXYZ.ShowSeriesGridlines = false;
    surfaceXYZ.ShowContourLines = true;
    chart1.ItemsSource = chartData;
    
    SeriesAttributes series0 = new SeriesAttributes();
    SeriesAttributes series1 = new SeriesAttributes();
    
    series0.GalleryAttributes = surfaceXYZ;
    series1.GalleryAttributes = surfaceXYZ;
    
    series0.BindingPath = "Score";
    series0.BindingPathX = "X";
    series1.BindingPath = "Y";
    
    chart1.Series.Add(series0);
    chart1.Series.Add(series1);

    Note how we need two series for an XYZ chart. The second series is bound to the Z data, but since we are looking to our surface from above in a two dimensional way, we will call it “Y” (or latitude, on a map). The Y axis represents the value we are plotting, the Score. If this was not a 2D representation of the chart (or contour) it would represent the depth or height. This is what we end up with.

    Untitled2

    I am sparing you from the cosmetic code, that can also be available if requested. If we make a few changes to our chart to make it a contour, this is what we will have.

    ChartFX.WPF.View3D view3D = chart1.View3D;
    view3D.IsEnabled = true;
    view3D.AngleX = -90;
    view3D.AngleY = 0;
    view3D.Projection = Projection.Orthographic;
    view3D.BackWallVisibility = Visibility.Collapsed;
    chart1.AxisX.Line.Visibility = Visibility.Hidden;
    chart1.AxisX.Grids.Major.Visibility = Visibility.Hidden;
    view3D.Lights.Clear();
    System.Windows.Media.Media3D.AmbientLight ambLight = 
        new System.Windows.Media.Media3D.AmbientLight(Color.FromRgb(0xD0, 0xD0, 0xD0));
    view3D.Lights.Add(ambLight);
    Untitled3

    If you have very sharp sight you will notice something. The “Florida shape” looks a little distorted. That happens because our contour was placed on a square, not keeping the ratio set by our chosen map. Also, the Max value of both the X and Z axis are not the values we used in our map. All that will distort our surface, making an unrealistic representation of our data. To fix that, here is the code necessary.

    chart1.AxisX.Max = 820; //Height of the bitmap
    chart1.AxisX.Min = 0;
    
    XYZExtension xyzExtension = new XYZExtension();
    xyzExtension.AxisZ.Max = 772; //Width of the bitmap
    xyzExtension.AxisZ.Min = 0;
    chart1.Extensions.Add(xyzExtension);
    
    chart1.View3D.Depth = 94.1463; //Width divided by Height

    Note that we use the XYZExtension to control the Max and Min of our Z axis. We then set both X and Z axis’ Max and Mix to the size of the map. We also changed the Depth property to the ratio of our map (almost a square, but not really). Changing the Depth property will control the size of the Z axis.

    Untitled4

    All we need now is to put the map on top! And here is the trick. We will create an ImageBrush out of the map, use it to paint the bottom of the chart (the X Axis) and relocate the bottom of the chart to the top.

    chart1.AxisX.Position = AxisPosition.Far;
    
    ImageBrush mapBrush = new ImageBrush();
    mapBrush.Stretch = Stretch.Fill;
    mapBrush.ImageSource = 
    new BitmapImage(new Uri("pack://siteoforigin:,,,/Resources/FloridaMap.png")); chart1.AxisX.Background = mapBrush; chart1.View3D.Wall.Thickness = 1;

    Untitled5

    Voilà! Our chart is ready. As mentioned before (more than a couple times, i guess), the necessary API to create a chart like this will be available on our next Service Pack, but if you can’t wait to play with it, download the hotfix at http://support.softwarefx.com/cfxwpf/update/hotfix/hotfix.htm.

    Posted Aug 06 2009, 06:02 PM by AndreG with 1 comment(s)
    Filed under:
  • Comparing multiple variables using a pane matrix (Part 1 of 2)

    There are many cases when the data to be plotted must be analyzed considering multiple variables, for example you might want to track sales of multiple products in multiple regions and encompassing multiple years. Let’s assume our data layer looks like this

    public class ProductSalesInfo
    {
        public double Sales { get; set; }
        public string Region { get; set; }
        public DateTime Date { get; set; }
        public string Product { get; set; }
    
        public int Year
        {
            get { return Date.Year; }
        }
    
        public int Month
        {
            get { return Date.Month; }
        }
    
        public string MonthName
        {
            get { return Date.ToString("MMM"); }
        }
    }

    We have added some helper properties (Year, Month and MonthName) as these will help us setting up our bindings. Our first approach to the problem includes using the crosstab data transform to make sure Chart FX plots one series per each unique combination of variables.

    <cfx:Chart Gallery="Line" x:Name="chart1" Style="{x:Static cfxMotifs:Basic.Style}">
      <cfx:Chart.DataTransforms>
        <cfxData:CrosstabTransform x:Name="crosstab1" 
    RowPath="Month" ColumnPath="Region,Product,Year"
    ValuePath="Sales" /> </cfx:Chart.DataTransforms> </cfx:Chart>

    PaneMatrix1

    As promised, we got a series per variable combination but it would be very difficult to detect trends in this picture, what we need is to layout multiple panes in such a way that each column in our grid represents a region and each row represents a product. Although we include a GridPanePanel as a way to layout multiple panes inside the chart, it has no knowledge about the nature of the data so it will just calculate a “round” number of rows and columns to accommodate the total number of panes. Also note that we do not sell ABC Widgets in Asia but if we want to create a matrix we will need to make sure we account for this.

    To help in this common scenario we have added some properties to CrosstabTransform, the most important one is a Boolean property called JoinOnColumns, when set to true, Crosstab will make sure that the series combinations are such that they can be easily plotted in a matrix, so it will add fake series where needed and also make sure that the ordering of the series is always the same. Crosstab also exposes a Dimensions property that allows you to query for each dimension found in the data, i.e. you can get a list of all the regions, products and years in your dataset.

    PaneMatrix2

    This chart will give you a better way to spot trends for a particular product or region. The labels for the columns and rows were achieved querying the Crosstab Dimensions on the DataBound chart event and using the GridPanePanel ColumnDefinitions and RowDefinitions collections. We have also removed the gridlines to reduce clutter. We have also hidden the series from the legend box and created a couple of custom legend items that show our third series dimension (year).

    In this sample there is a potential problem, crosstab goes through the data and each time it finds a new Row (in this case a new Month) it adds it to a list but it will not order this list by default, this means that if the first element in your data is a sale in March this will become the first “row” so the X axis will show the months out of order, although you could maybe workaround this by making sure your data is ordered by month we have added a SortRows property that will take care of this for you, this also means we have made a Sorted property obsolete and replaced it with a hopefully more descriptive name SortColumns.

    Additionally we have added a RowIDPath property which allows you to sort your rows using the Month index but show the Month Name in your axis.

    <cfx:Chart.DataTransforms>
      <cfxData:CrosstabTransform x:Name="crosstab1"
    RowIDPath="Month" RowPath="MonthName" SortRows="True"
    ColumnPath="Region,Product,Year" JoinOnColumns="True"
    ValuePath="Sales"
    /> </cfx:Chart.DataTransforms>
    PaneMatrix3 

    At this point we have taken care of the X axis which now shows friendlier month abbreviations and will be ordered by month index regardless of the order in the data but we have manipulated our random data to show another issue, sales of “ABC Widget” are in the thousands while “Contoso Tool” sales are in the hundreds and because we are using a single Y axis it is very hard to note any trends in the lower row.

    This issue can be solved in two ways, first you could use a different Y axis per row, this will still allow you to focus on a product and compare different regions using the same axis while making sure that each products gets its own Y axis.

    PaneMatrix4

    Alternatively, you could decide that you want to have a different axis per pane, this will make same-row comparisons more difficult but might be helpful if a product sales varies widely by region. Instead of totally hiding the Y axis we will show only the min and max labels inside the plot area.

     PaneMatrix5

     

    We hope this post along with the changes we have made to the crosstab transform will help you create charts where users can analyze the data more effectively, note that you need Chart FX build 3504 or later. The code used to configure these charts was not included in the text of the post to keep it to a manageable size but it is provided as an attachment.

    JuanC

    Posted Aug 05 2009, 03:17 PM by JuanC with 2 comment(s)
    Filed under:
Copyright 2008 Software FX, Inc.