Wednesday, 9 February 2011

Silverlight 4 Toolkit Chart Zoom and Pan Extension

Silverlight 4 Toolkit Chart Zoom and Pan Extension






Features
  • Utilises existing toolkit without modification
  • Zoom box functionality
  • Mouse wheel zoom functionality
  • Pan functionality
  • Scroll Functionality
  • X Span functionality
Overview
I've been working on a project which uses the Silverlight 4 Toolkit chart control. The control is great, however there is no real zoom functionality which is a problem.

Here is an example given where the charts template is moidified to provide a ScrollViewer wrapper for the chart panel which allows you to zoom the whole chart and scroll around it:

http://www.silverlight.net/content/samples/sl4/toolkitcontrolsamples/run/default.html

[DataVisualization -> Zoom]

The big problem with this is that the axes are scrolled with the chart surface, which is not the desired effect.

Solution
I thought about this for a while and came up with a solution without modifying the source.

It was clear that the problem stemmed from the fact that the axes were not independent of the plot surface, but this could be easily solved by using a grid with 3 plot surfaces to hold the main plot surface and X and Y axes:


The scroll behaviour of the axes plot surfaces can be tied to the main plot surface to make it appear as though they were one piece!

Layout
Each panel requires a chart control and a customised template. The controls are added to the grid. The next step is to hide the axes on the main plot surface, this can't be done manipulating individual templates for each of the 3 chart areas and styling out the axis features:

<Style x:Key ="zoomChartMajorTickMarkStyle" TargetType="Line">
                <Setter Property="Visibility" Value="Collapsed" />
            </Style>
            <Style x:Key ="zoomChartAxisLabelStyle" TargetType="chartingToolkit:AxisLabel">
                <Setter Property="Visibility" Value="Collapsed" />
            </Style>
            <Style x:Key ="zoomChartHiddenPointStyle" TargetType="chartingToolkit:LineDataPoint">
                <Setter Property="Visibility" Value="Collapsed" />
            </Style>

The x and y templates are then stripped down to leave just the main named surface and a ScrollViewer:

<ControlTemplate TargetType="chartingToolkit:Chart" x:Key="ZoomXTemplate">
                <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="0">
                    <Grid>
                        <ScrollViewer
                        x:Name="ChartScrollArea" BorderThickness="0"
                        HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Hidden" LayoutUpdated="XScrollArea_LayoutUpdated">
                            <chartingPrimitivesToolkit:EdgePanel x:Name="ChartArea" Style="{TemplateBinding ChartAreaStyle}" />
                        </ScrollViewer>
                    </Grid>
                </Border>
            </ControlTemplate>

The main chart template has a ScrollViewer, but also a canvas and a rectangle added for a zoom box:

<ControlTemplate TargetType="chartingToolkit:Chart" x:Key="ZoomChartTemplate">
                <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}" Padding="0">
                    <Grid>
                        <Canvas x:Name="ZoomCanvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Canvas.ZIndex="2">
                            <Rectangle x:Name="ZoomBox" Height="100" Width="100"
                                   Canvas.ZIndex="2" Fill="#FFA3AEB9"  Opacity="0.4" Visibility="Collapsed"
                                   Canvas.Left="10" Canvas.Top="10">
                            </Rectangle>
                        </Canvas>
                        <ScrollViewer x:Name="ChartScrollArea" BorderThickness="0"
                        HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" MouseLeftButtonUp="ChartScrollArea_MouseLeftButtonUp" MouseLeftButtonDown="ChartScrollArea_MouseLeftButtonDown" Loaded="ChartScrollArea_Loaded" MouseMove="ChartScrollArea_MouseMove">
                            <chartingPrimitivesToolkit:EdgePanel x:Name="ChartArea" Style="{TemplateBinding ChartAreaStyle}">
                                <Grid Canvas.ZIndex="-1" Style="{TemplateBinding PlotAreaStyle}" />
                                <Border Canvas.ZIndex="1" BorderBrush="#FF919191" BorderThickness="1" />
                            </chartingPrimitivesToolkit:EdgePanel>
                        </ScrollViewer>
                    </Grid>
                </Border>
            </ControlTemplate>

Code
There is a lot of code for working out the zoom geometry (this took some time to work out, but works nicely) which can be seen in the full code, but the main point to note is when the data is bound to the chart, the X and Y axes have a dummy series placed on them with 2 data points at their extremities to make them size correctly and match the main chart. Here is how the X axis is configured:

private void SizeXAxis(DateTime min, DateTime max)
        {
            // Set X axis limits with a dummy series
            List<AxisPoint> points = new List<AxisPoint>();
            points.Add(new AxisPoint(min, 0));
            points.Add(new AxisPoint(max, 0));
            LineSeries xSeries = new LineSeries();
            xSeries.ItemsSource = points;
            xSeries.IndependentValueBinding = new System.Windows.Data.Binding("Date");
            xSeries.DependentValueBinding = new System.Windows.Data.Binding("Value");
            xSeries.Width = 0;
            xSeries.DataPointStyle = this.Resources["zoomChartHiddenPointStyle"] as Style;
            this.ZoomXAxis.Series.Add(xSeries);
        }

Full XAML
The XAML and C# basically recycle the WidgetPopularityPollCollection class and the GizmoPopularityPollCollectionon class from the Silverlight Toolkit 4 samples. So it should be fairly easy to get working. I'll try and get the full solution uploaded somewhere.


Full C#


Conclusion
This has turned out to be quite a good modification to the existing toolkit and hopefully should be helpful to others out there!

30 comments:

  1. Hey Geoff, nice post and very good insights into how to customize the toolkit without a lot of custom coding!

    I was wondering if during your interactions with the charting toolkit you figured out how to display only the start (minimum) and end (maximum) dates in a DateTimeAxis for a line series.

    This seems like such a trivial question, yet I'm having a hard time with it. If you have any ideas, please have a look at the question I posted here: http://stackoverflow.com/questions/6709794/how-to-display-minimum-and-maximum-values-for-datetimeaxis-or-linearaxis-in-silve

    That will give you an idea of my progress towards solving the problem so far.

    Thanks in advance for the help and for the great post!

    ReplyDelete
  2. Hi Victor, I wouldn't think you could achieve this with the built in axis as it's not designed to do this. However you could potentially do this by styling out the x axis on the chart like I do in this example, then create your own pseudo axis by placing two textblocks underneath external to the chart at each end; it may take some time to get the formatting right but will definitely work as it's a simple work-around. You could use LINQ to get the min and max when the data is loaded and bind to these values.

    ReplyDelete
  3. Hey Geoff,

    Big thanks for the answer! It's funny because that's exactly what I ended up doing yesterday, inspired by what you did here.

    I essentially customized the Template for DateTimeAxis and "faked" the date range by adding a grid with two columns, where the left columns in bound to my min value and the right column is bound to the max value. I even added a couple of "Line" controls for the tickmarks.

    I am almost satisfied with what I have. The perfectionist in me doesn't like the fact that I am binding my min/max textblocks to properties in my view model. Ideally, I should be able to do something like:

    <TextBlock Text="{TemplateBinding ActualMinimum}" />

    instead of:

    <TextBlock Text="{Binding strMinDateFromViewModel}" />

    That would allow me to leave my charting styles in a generic xaml resource dictionary, instead of having to make them aware of my view model .cs file.

    If you have any ideas why the template binding doesn't work, I am all ears. In any case, many, many thanks for taking the time to offer an apt answer my question!

    Regards!
    Victor

    ReplyDelete
  4. Hi Victor, Glad to hear you cracked it! The reason you can't add those properties to the template is that they aren't part of the original templatable control. You could create your own control based on the chart and add dependency props for these or have you tried using a valueconverter so you can bind the template data source prop (whatever it's called) and then pull out the min or max throught the converter (you'd need a separate converter for min and max).

    ReplyDelete
  5. Hey there, I found your zoom solution and have been trying to implement it and have been having trouble.
    I recently just made a breakthrough integrating my MultiChart control but then noticed that the legend is missing. Any quick solutions or will I have to make another panel for it?

    ReplyDelete
  6. Hi Suiko6272, I stripped out the bits of the ControlTemplate I didn't want. This is what a chart ControlTemplate looks like before any edits, so you can put back in the bits you want:

    ReplyDelete
  7. http://geoffwebbercross.blogspot.com/2011/08/toolkit-chart-controltemplate.html

    ReplyDelete
  8. Well I found that your control template for ZoomChartTemplate gets rid of it and seems like if I can just figure out what the actual object is I could add it there, but for the life of me I can't figure it out. I'm still fairly new to doing xaml templates, I'm more of the database and code behind guy.
    If you read this before I figure it out do you know the object to use?

    ReplyDelete
  9. Thank you ^^, forgot to refresh before posting.

    ReplyDelete
  10. Thanks for the information. I am new to silver light. I have successfully created a chart with line and area graphs and the zoom with slider shown on the Silverlight toolkit. More over, I would like to add pan and zoom box ( and possible zoom buttons)
    Can you please share your code? I would really appreciate it.

    ReplyDelete
  11. Here you go:
    http://slchartzoomandpan.codeplex.com/

    ReplyDelete
  12. Hi Goeff, We could implement the solution provided by you and it is a bg help! Thanks!

    However, we are stuck up with a major problem. The X Axis in our case is linear and represents time in hours. When we add this there is an offset of about 0.625 hrs in the chart plotted. Can you help us identify why could this happen?

    Appreciate you help!

    ReplyDelete
  13. If you can get me a copy of the code which will run without any external dependencies, send it here and I'll take a look:

    geoffwebbercross@gmail.com

    ReplyDelete
  14. Hi Geoff, could do it! Was a real stupid mistake, we converted the X Axis to linear axis and were sending in string values instead of double!!! so the offset!

    Thanks!

    ReplyDelete
  15. Hi Geoff,
    Now I'm using WCF Service and ADO.NET Entity to connect database, but can't put WCF Service's e.result of TIMEDIFF in to the TimeSpan.
    So I'm wondering to know how to put TIMEDIFF in TimeSpan, thanks!

    Best regards!

    ReplyDelete
  16. Hi Pulini,
    What type of object is TIMEDIFF, is it a database field type? Also what are you using the TimeSpan for - the chart span option?

    Cheers

    ReplyDelete
  17. Hi Geoff,
    Sorry, it's DATEDIFF not TimeDIFF.That's my miss .Yes,I use TimeSpan for the chart span option.Thank you.
    Best regards!

    ReplyDelete
  18. Well, if you use a datepart of hh for example (http://msdn.microsoft.com/en-us/library/ms189794.aspx) the result will be in hours, so you can create a timespan like this: new TimeSpan(hrs, 0, 0). Where hrs is the result of DATEDIFF function.

    ReplyDelete
  19. Hi. It is a really nice article and very useful.
    I want to ask you however if I can use your project with SL 4. When I trz to load it with VS 2010 it gives me the error that I must upgrade mz VS to use Silverlight Tools 5 for VS.
    Our client wants to use SL4 further on so I want to ask you if there is any functionality that is specific to SL5 in your code.
    Thanks.

    ReplyDelete
  20. Hi Andrei,
    It will all work in SL4, there is actually an SL4 branch, but there are more updates on the trunk which is SL5.

    Cheers,
    Geoff

    ReplyDelete
  21. As far as I could see, the branch for SL4 contains only empty folders, so I cannot use that in a VS solution.
    Did you move everything to SL5 ?

    ReplyDelete
  22. I'll check it when I get home, the main code will work with SL4 if you just change the target Silverlight version in the project properties. I'd recommend using this anyway as it has some improvements.

    ReplyDelete
  23. Hi Geoff,
    I managed to run the project in SL4 and to see how the panning works for the chart. I cannot make use of SL5 yet because I have to install the Service Pack for VS 2010 and I don't have enough place in my disk :-)

    In other matter of speaking, I want to ask you if the following scenario can be done using your project:

    I have 30 days plotted on the chart ( a collection of 30 points between 1/1/2012 and 30/1/2012 ) and when I pan towards the minimum value I want to see the dates from December also.

    For a better understanding of what I need to do please take a look at this Yahoo! Finance chart. Our client wants the exact functionality of panning ( and scaling ) like the one at this link:

    http://uk.finance.yahoo.com/echarts?s=MSFT#symbol=msft;range=1m;compare=;indicator=volume;charttype=area;crosshair=on;ohlcvalues=0;logscale=off;source=;

    ReplyDelete
  24. Hi Goeff, We could implement the solution provided by you and it is a bg help! Thanks!
    now the problem was that when i zoom chart for 5days it displays only time on X-Axis but i want Date and Time both for less then 5Days chart zoom, how can i solve this problem?

    ReplyDelete
  25. You should be able to format the labels like this:
    http://forums.silverlight.net/t/89751.aspx/1

    ReplyDelete
  26. hi Geoff Thanx for your reply,
    i managed this issue but now my major problem is,
    in my data many times NaN(Not A Number value) occurs & i want to manage that value by not showing that value on chart, means if data1 is value, data2 is value, data3 is NaN, data4 is value.
    when i generate graph show line on data1,data2,and then after it continue directly on data 4 and no any type of line show on data2 to data3 and data3 to data4.

    ReplyDelete
  27. You could try processing the data first and removing the NaNs before binding?

    ReplyDelete
  28. Geoff...

    Hell of a solution !

    Thank you so much for sharing your brilliance, saved me a lot of time and with few alterations i have exactly what i need .

    All the best bud !

    Thanks again so much
    Marco

    ReplyDelete