Adding a Location Crosshair to Silverlight charts (again!)

Silverlight is moving fast. Really fast. The recent MIX09 conference saw the release of Silverlight 3 (Beta) and also a new release of the Silverlight Toolkit. All this change is making it hard for us bloggers to keep up!

Just over a month ago I posted an article on this blog about how to add a location crosshair to the Silverlight Toolkit Charts. This blog post briefly discusses how the charts have changed with the release last week, and updates the code accordingly.

If you want to know how the general approach works, I would recommend reading the previous blog post, this post serves as an update and discussion on how the chart template has changed.

There have been a number of minor changes to the charts that affect my previous implementation, including a change of namespace and orientation type on the chart axes. However, the biggest change is in the template of the chart control itself. Previously the chart template was structured via a Grid as shown below:

<ControlTemplate TargetType="charting:Chart">
    <Grid Name="ChartArea" Style="{TemplateBinding ChartAreaStyle}">

        <!-- ##### chart control adds column / row definitions and axes here #### -->
        
        <Grid Height="250" x:Name="PlotArea" Style="{TemplateBinding PlotAreaStyle}">

            <!-- the standard chart template components -->
            <Grid x:Name="GridLinesContainer" />
            <Grid x:Name="SeriesContainer"/>
            <Border BorderBrush="#FF919191" BorderThickness="1" />

            <!-- ##### I added cross hair and legend here #### -->
        </Grid>
    </Grid>
</ControlTemplate>

After loading the template the chart adds the axes and the appropriate column / row definitions. This leaves us free to add new visual content withing the 'PlotArea' grid. Inspecting the visual tree, with Silverlight Spy, it look like the following:

visualtree-before

The content I added in order to construct a crosshair and legend are highlighted in red.

The March release of the Silverlight toolkit modifies the way in which axes are added to the chart by the introduction of a new layout panel, the EdgePanel (think DockPanel!). This panel allows you to locate (or dock) elements at one of the four edges of a panel or the panel centre. The modified control template is show below:

<ControlTemplate TargetType="charting:Chart" x:Key="ChartTemplate">
    <Grid x:Name="ChartRoot" Style="{TemplateBinding PlotAreaStyle}">

        <chartingprimitives:EdgePanel x:Name="ChartArea" Style="{TemplateBinding ChartAreaStyle}">                                   
          
            <Grid Canvas.ZIndex="1" Style="{TemplateBinding PlotAreaStyle}" />
            <Border Canvas.ZIndex="1" BorderBrush="#FF919191" BorderThickness="1" />

            <!-- ##### I added cross hair and legend here #### -->
            
            <!-- ##### chart control adds column / row definitions and axes here #### -->
            
        </chartingprimitives:EdgePanel>
    </Grid>
</ControlTemplate>

I have indicated the location where I add my own visual content and also the location where the chart control adds the axes. The resulting visual tree is shown below:

visualtree-after

The significant difference here is that all the components of our chart are contained within the same EdgePanel, with their location dictated by their EdgePanel.Edge property. One problem this does introduce for us is that of Z-ordering, previously our additional content was top of the stack, now it sits somewhere in between. Fortunately EdgePanel honours the Canvas.ZIndex attached property allowing us to push our content to the top. The modified template is given below in full:

<ControlTemplate TargetType="charting:Chart" x:Key="ChartTemplate">
    <Grid x:Name="ChartRoot" Style="{TemplateBinding PlotAreaStyle}">

        <chartingprimitives:EdgePanel x:Name="ChartArea" Style="{TemplateBinding ChartAreaStyle}">                                   
          
            <Grid Canvas.ZIndex="-1" Style="{TemplateBinding PlotAreaStyle}" />
            <Border Canvas.ZIndex="3" BorderBrush="#FF919191" BorderThickness="1" />

            <!-- a location crosshair -->
            <Grid Name="CrosshairContainer" Canvas.ZIndex="1" Background="Transparent"
                 MouseMove="CrosshairContainer_MouseMove" MouseEnter="CrosshairContainer_MouseEnter"
                 MouseLeave="CrosshairContainer_MouseLeave" >
                <Grid Name="Crosshair">
                    <Line Name="Vertical" X1="{Binding Path=X}" Y1="0" X2="{Binding Path=X}" Y2="400" Stroke="Black"/>
                    <Line Name="Horizontal" X1="0" Y1="{Binding Path=Y}" X2="400" Y2="{Binding Path=Y}" Stroke="Black"/>
                </Grid>
            </Grid>

            <!-- a location 'legend' -->
            <Border Canvas.ZIndex="2" Name="LocationIndicator" Visibility="Collapsed" Style="{StaticResource LocationLegendStyle}">
                <StackPanel Orientation="Horizontal" Margin="5">
                    <TextBlock Text="Location: "/>
                    <TextBlock Text="{Binding Path=Key,
                                Converter={StaticResource FormattingConverter}, ConverterParameter=hh:mm:ss}"/>
                    <TextBlock Text=", "/>
                    <TextBlock Text="{Binding Path=Value,
                                Converter={StaticResource FormattingConverter}, ConverterParameter=0.00}"/>
                </StackPanel>
            </Border>
            
        </chartingprimitives:EdgePanel>
    </Grid>
</ControlTemplate>

One more subtle point, the event handlers for MouseMove, MouseLeave etc.. when associated with a Grid only work if the background is not null, hence the need for a transparent background. I have no idea why, believe me ... it just works!

One final important change is the method used to translate points from screen coordinates into a position within the chart coordinate system. Previously I used a method called GetPlotAreaCoordinateValueRange on the 'hidden IRangeAxis interface. This has now been renamed to GetValueAtPosition and returns and takes a UnitValue type as its input (presumably for API consistency with the pie charts). Here is the code:

/// <summary>
/// Transforms the supplied position on the plot area grid into a point within
/// the plot area coordinate system
/// </summary>
private KeyValuePair<DateTime, double> GetPlotAreaCoordinates(Point position)
{
    IComparable yAxisHit = ((IRangeAxis)YAxis).GetValueAtPosition(
        new UnitValue(PlotArea.ActualHeight - position.Y, Unit.Pixels));

    IComparable xAxisHit = ((IRangeAxis)XAxis).GetValueAtPosition(
        new UnitValue(position.X, Unit.Pixels));

    return new KeyValuePair<DateTime, double>((DateTime)xAxisHit, (double)yAxisHit);
}

With these changes in place, our crosshair is fully functional once more:

... until the next release of the toolkit ;-)

You can download the project sourcecode: sllinechartcrosshairupdated.zip.

Regards, Colin E.

blog comments powered by Disqus