Adding a Location Crosshair to Silverlight charts (again!)

March 23rd, 2009 by Colin Eberhardt

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.

Tags: ,

26 Responses to “Adding a Location Crosshair to Silverlight charts (again!)”

  1. Andrei says:

    Hi.
    Very nice article !

    I want to ask you if I can replace your marker that is displayed on the chart on mouse move with a circle like the one used by Yahoo Finance charts:

    http://finance.yahoo.com/echarts?s=GOOG

    Any advice/tip would be very appreciated.
    Thanks in advance.

  2. swapnil says:

    Hi!This is awesome article.I downloaded it ;but i am getting error at references System.Windows.Controls.DataVisualization.Toolkit.I have it in wpf toolkit.Also i am getting error at debugging while adding datetime axis in project.It asks browse path for datetime axis.Plz help.

  3. [...] the Silverlight toolkit charts in the past and have written a few blog posts which detail how to add interactivity to the charts. The early releases were pretty slow, due to the large number of ‘Control’ instances that the [...]

  4. giovanni says:

    hi
    is this code compatible with silverlight 4.0?

  5. Matthias says:

    The XAML was removed in my last post :-( .

    Does’n work:

    Work (a while):

  6. Matthias says:

    Hi,

    I try out But I get just a vertical line in the middle of the chart and i can’t move them by moving the mouse. Therefore I try out . As long my chart is smaller than 2000px it should work.

  7. Matthias says:

    Very nice article. I have just one question. How can I bind the x2 and x2 value of the horizontal and vertical line of the crosshair to the chartsize? I try X2=”{Binding ActualWidth, ElementName=Chart}” and Y2=”{Binding ActualHeight, ElementName=Chart}”. But the lines become invisible. Have you an idea?

    Thanks
    Matthias

  8. Alexandra says:

    This is an awesome article as almost solved a problem I was facing. I am working on a chart that needs to have the crosshair that doesn’t follow the mouse but follows the actual values on the chart. I am trying to find a neat way of accessing actual pixel values of the Series because it seems like such a waste to do this:

    current coordinate -> value @ curr. coordinate -> nearest (actual data) value in series -> coordinate of that value -> pick the coordinate closest to current.

    Do you think there could be a more elegant way to do this than just jumping between IRangeAxis.GetValueAtPosition(…) and IAxis.GetPlotAreaCoordinate(…)?

    In any case, thanks for a great article!!!

  9. Cory Plotts says:

    Nice article! Exactly what I was looking for (a usage of GetValueAtPosition).

  10. Andy says:

    it seems to happen only when i collapse the y-axis. interesting, well, thanks for trying :)

  11. Andy says:

    Hey I implemented this design (which is awesome by the way), but I’m having some issues. I have multiple lineseries on the chart and sometimes (actually quite often) the chart paints one line less than it should. If I mouse over the chart the missing line “magically” appears. I can’t figure it out my code looks fine have you seen anything like this?

  12. Great post! Just wanted to let you know you have a new subscriber- me!

  13. [...] shows how to add crosshairs on top of a Chart to display the coordinates of the mouse pointer. Adding a Location Crosshair to Silverlight charts (again!) – Colin Eberhardt updates his crosshairs post to accommodate changes in the March 09 release. How [...]

  14. Anthony Brien says:

    How would you go about to show a line on the chart at a specific dependent/independent value.

    For example, if in the above chart, I want to show a vertical line at 10:00 AM or an horizontal line at 6080. I want to do this in one of my charts to show when we go over a known budget value.

    I’ve tried inspiring myself from this sample, but I can’t figure out how to do the opposite of IRangeAxis.GetValueAtPosition(). I want to bind my “budget line” onto a dependent/independent value, but I dont know how to convert that to a pixel location on the chart.

    Thanks

    • Hi Anthony,

      Glad you liked this blog post. To answer you question, prior to the March ’09 there was nothing exposed on the charting API for converting a value into a coordinate on the chart surface. However, if you know the width of your PlotArea and your axis Minimum and Maximum, these is a pretty easy transformation to do yourself.

      However, with the March ’09 release, things just got easier! Take a look at IAxis.GetPlotAreaCoordinate(object value):

      http://silverlight.codeplex.com/SourceControl/changeset/view/18791#507616

      Regards, Colin E.

  15. Anthony Brien says:

    Thanks Colin! I’ve been starting to use this charting library for WPF and your post is the best I found to show how to customize these charts, like removing the titles, legends. I’ll use your technique for adding the crosshair lines to add a “budget” line in a few of my charts.

  16. Adding a Location Crosshair to Silverlight charts (again!) – Colin Eberhardt…

    Thank you for submitting this cool story – Trackback from DotNetShoutout…

  17. [...] Adding a Location Crosshair to Silverlight charts (again!) [...]

Leave a Reply