This is a migrated thread and some comments may be shown as answers.

How do I let the user interact with chart annotations?

9 Answers 223 Views
ChartView
This is a migrated thread and some comments may be shown as answers.
Joe
Top achievements
Rank 2
Iron
Iron
Veteran
Joe asked on 17 Aug 2018, 07:52 PM

I have a C# WPF application using MVVM with a simple RadCartesianChart that shows a single ScatterLineSeries representing a surface profile.  It works well. 

Now I want to enable my user to mark out certain horizontal regions of the plot for use in my application.  The general idea is this

  1. User clicks a button that adds a visual indicator representing the region. 
  2. The indicator appears.  It looks like a big wide bar going from the bottom Y of the plot to the top Y and covering an arbitrary X width.
  3. The user can then use the mouse (or their finger) to made the bar wider/narrower or move it left or right across the plot

(I have attached an image showing roughly what this should look like.  It is from a much older version of this application that doesn't use C#, WPF or Telerik.)

My problem is not in writing interaction/manipulation code.  It is in determining exactly what Telerik entity I should interact with.  That is I don't know how best to achieve this with the tools Telerik gives me.   

I can think of several approaches. 

  • For example, I looked into using Chart Annotations.   They seem perfect for marking out chart regions and your examples are good.  But I can't find any examples that show me how the user might *interact* with them.    How do I add a mouse/touch handler for an annotation?   Is it possible?  Is that the best way?
  • Or maybe I should add mouse/touch handlers for the RadCartesianChart object.  Then in code-behind I could try to figure out what annotation my mouse/finger is over and go from there, is that the way?  
  • Or should I instead try to create my own buttons, completely separate from the cahrt that overlay the whole plot and match up exactly with chart plot area.  Is that the way to go?
  • Is there some other, cleaner way to achieve this?

 

My Chart in XAML is like this:

   

<tk:RadCartesianChart x:Name="Chart"
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch"
                         Background="{StaticResource GsBackgroundDark}"
                         Foreground="{StaticResource GsForegroundLight}"
                         Height="300"
                         HoverMode="FadeOtherSeries"     
                         SelectionPalette="FlowerSelected"
                         ga:ChartUtilities.IsDragToSelectEnabled="True"
                         ga:ChartUtilities.SelectionRectangleStyle="{StaticResource SelectionRectangleStyle}"
                          
                         >
       <tk:RadCartesianChart.Resources>
           <Style x:Key="LabelStyle" TargetType="TextBlock">
               <Setter Property="Foreground" Value="{StaticResource GsForegroundLight}"/>
           </Style>
       </tk:RadCartesianChart.Resources>
       <tk:RadCartesianChart.HorizontalAxis>
           <tk:LinearAxis Title="{Binding Path=XAxisTitle}"
                          Foreground="{StaticResource GsForegroundLight}"
                          LabelStyle="{StaticResource LabelStyle}"
                          LabelFormat="0.00"
                          />
       </tk:RadCartesianChart.HorizontalAxis>
       <tk:RadCartesianChart.VerticalAxis>
           <tk:LinearAxis  x:Name="YAxis"
                           Title="{Binding Path=YAxisTitle}"
                           Foreground="{StaticResource GsForegroundLight}"
                           LabelStyle="{StaticResource LabelStyle}"
                           LabelFormat="0.00"   
                           />
       </tk:RadCartesianChart.VerticalAxis>
 
           <tk:RadCartesianChart.AnnotationsProvider>
               <tk:ChartAnnotationsProvider Source="{Binding Regions}">
                   <tk:ChartAnnotationDescriptor
                       d:DataContext="{d:DesignInstance gavm:ProfileRegionVm}">
                       <tk:ChartAnnotationDescriptor.Style>
                           <Style TargetType="tk:CartesianMarkedZoneAnnotation">
                               <Setter Property="VerticalFrom" Value="-1000" />
                               <Setter Property="VerticalTo" Value="1000" />
                               <Setter Property="HorizontalFrom" Value="{Binding HorizontalFrom}"/>
                               <Setter Property="HorizontalTo" Value="{Binding HorizontalTo}"/>
                               <Setter Property="Stroke" Value="Yellow"/>
                           </Style>
                       </tk:ChartAnnotationDescriptor.Style>
                   </tk:ChartAnnotationDescriptor>
               </tk:ChartAnnotationsProvider>
           </tk:RadCartesianChart.AnnotationsProvider>
            
       <tk:RadCartesianChart.Series>
           <tk:ScatterLineSeries ItemsSource="{Binding Path=ProfilePointsConverted}"
                                 XValueBinding="X"
                                 YValueBinding="Y"                          
                                 >
               <tk:ScatterLineSeries.LegendSettings>
                   <tk:SeriesLegendSettings Title="Profile"/>
               </tk:ScatterLineSeries.LegendSettings>
           </tk:ScatterLineSeries>
       </tk:RadCartesianChart.Series>
   </tk:RadCartesianChart>

My ProfileRegionVm class -- which represents an instance of this horizontal reagion is like this:

using System.Collections.Generic;
using GApp.Core.ViewModels;
using SWPoint = System.Windows.Point;
 
namespace GApp.Analyze.ViewModels
{
    public class ProfileRegionVm : BaseVm
    {
 
        public ProfileRegionVm()
        {
        }
 
        private List<SWPoint> _profilePoints;
 
        private List<SWPoint> Points
        {
            get => _profilePoints;
            set => SetProperty(ref _profilePoints, value);
        }
 
        private Sdk.Line _line;
 
        private Sdk.Line Line
        {
            get => _line;
            set => SetProperty(ref _line, value);
        }
 
        private string _name;
        public string Name
        {
            get => _name;
            set => SetProperty(ref _name, value);
        }
 
        private double _verticalFrom;
        public double VerticalFrom
        {
            get => _verticalFrom;
            set => SetProperty(ref _verticalFrom, value);
        }
 
 
        private double _verticalTo;
 
        public double VerticalTo
        {
            get => _verticalTo;
            set => SetProperty(ref _verticalTo, value);
        }
 
        private double _horizontalFrom;
 
        public double HorizontalFrom
        {
            get => _horizontalFrom;
            set => SetProperty(ref _horizontalFrom, value);
        }
 
 
        private double _horizontalTo;
 
        public double HorizontalTo
        {
            get => _horizontalTo;
            set => SetProperty(ref _horizontalTo, value);
        }
 
    }
}




9 Answers, 1 is accepted

Sort by
0
Accepted
Martin Ivanov
Telerik team
answered on 22 Aug 2018, 01:27 PM
Hi Joe,

The most convenient approach would be to use the chart's annotations. But they don't support user interactions so you will need to implement this using the mouse events. Which mouse events you will use is up to your preferences. For example, you can use the annotation events but in this case you will need to capture the mouse/touch input while dragging so that the MouseMove will be reported for the annotation. Otherwise, you can use the chart's mouse events.

I've attached a small example that shows an interactive annotation. I hope this helps you to proceed with your implementation. Note that the example is not finished and you will need to finish the implementation, but it shows the main idea.

Regards,
Martin Ivanov
Progress Telerik
Get quickly onboarded and successful with your Telerik and/or Kendo UI products with the Virtual Classroom free technical training, available to all active customers. Learn More.
0
Joe
Top achievements
Rank 2
Iron
Iron
Veteran
answered on 22 Aug 2018, 05:17 PM

Wow!  That answer was well above and beyond my expectations!  Exactly what I needed. 

 

Thank you, Martin.  This must have taken you some time and I appreciate the effort.

0
Martin Ivanov
Telerik team
answered on 22 Aug 2018, 09:37 PM
Hello Joe,

I am really happy to hear that the project was helpful. Hopefully, this will be enough to achieve your requirement. If you have other questions let me know.

Regards,
Martin Ivanov
Progress Telerik
Get quickly onboarded and successful with your Telerik and/or Kendo UI products with the Virtual Classroom free technical training, available to all active customers. Learn More.
0
Joe
Top achievements
Rank 2
Iron
Iron
Veteran
answered on 23 Aug 2018, 05:51 PM

Hi Martin,

I know I marked this as answered and I do still consider it so, but I will admit I still have a simpler but related question.

I see what you are doing in code-behind.  Since I'm using MVVM, I was kind of hoping to do be able to more in XAML.   My hope was to mimic what you do only via sort of  DataTemplate for each of my ProfileRegionVm objects.  Each instance of the template would show the 3 annotations you created in code-behind. (one PlotBandAnnotation and the two GridLineAnnotations for the handles) for each of my objects.  

This how I typically display collections of items in WPF:  ListView bound to some ItemsSource with a DataTemplate showing each item.

Is it possible to do something like this with RadCartesianChart? It would-require some sort of input-collection property on the chart with an "ItemsSource" property that I could bind to my list of ProfileRegionVm, but I don't see any such property.  

All I can see is a ChartAnnotationsProvider.  But that just seems to let me set a style on existing single ChartAnnotations.  I need 3 annotations per region.

Am I correct in understanding that I will have to do all of this in code-behind?

<DataTemplate DataType="{x:Type gavm:ProfileRegionVm}">
    <Grid>
        <tk:CartesianPlotBandAnnotation Axis="{Binding ElementName=HorizontalAxis}"
                                        From="{Binding HorizontalFrom, Mode=TwoWay}"
                                        To="{Binding HorizontalTo, Mode=TwoWay}"/>
        <tk:CartesianGridLineAnnotation Axis="{Binding ElementName=HorizontalAxis}"
                                        Value="{Binding HorizontalFrom, Mode=TwoWay}"/>
        <tk:CartesianGridLineAnnotation Axis="{Binding ElementName=HorizontalAxis}"
                                        Value="{Binding HorizontalTo, Mode=TwoWay}"/>
    </Grid>
</DataTemplate>

 

The problem came in trying to tie some collection to the Chart.  I don't
It seems that

0
Joe
Top achievements
Rank 2
Iron
Iron
Veteran
answered on 23 Aug 2018, 06:01 PM
I should that that I'm not looking for code, you've already done plenty.  I'm just hoping for a quick bit of advice on verbal guidance

(Also that last post had some extraneous sentences on the end after the template example  Didn't realize you can't edit these things once posted...)
0
Joe
Top achievements
Rank 2
Iron
Iron
Veteran
answered on 24 Aug 2018, 02:11 PM
Actually I think I am OK.  I finally stumbled on your Git-Hub XAML-SDK examples and it has more different ways of working with charts and annotations than I could have imagined!
0
Martin Ivanov
Telerik team
answered on 24 Aug 2018, 03:41 PM
Hello Joe,

It is great that you found the SDKs. I hope this will help you further with your scenario. 

As for using the annotations in MVVM, you can do this with the AnnotationsProvider. The idea here would be to define an annotation model or few models. And populate a collection that holds the annotation models in the view model. Then you can use a descriptor selector to select a different descriptor based on your model. The small struggle with this approach would be that you will need to somehow group the models. This is because you will need a single collection to bind to the provider, and on the other hand each zone is created by 3 annotations.

If you have further questions on this let me know.

Regards,
Martin Ivanov
Progress Telerik
Get quickly onboarded and successful with your Telerik and/or Kendo UI products with the Virtual Classroom free technical training, available to all active customers. Learn More.
0
2Plus
Top achievements
Rank 1
answered on 07 Nov 2019, 04:59 PM
Hello Joe

I have to develop a component similar to the one you did. For now I have solved it but there is too much coupling between view and viewmodel and some work in the code behind that I want do delete.
Now I'm looking for a better solution, do you have any advice for me?
0
Joe
Top achievements
Rank 2
Iron
Iron
Veteran
answered on 07 Nov 2019, 05:47 PM

Hi Matt,

I also have a good deal of code-behind.  I use annotations to denote regions which have mouse/touch handlers for dragging the entire region/annotation left or right.  I also use two smaller annotations to denote the "handles" of the currently selected region, so that the user can make it wider/thinner.  It all relies on knowing specifically what type is the current DataContext of a given annotation and chart.   For example, here is a MouseMove handler from one of my "handle" annotations

private void RegionHandle_OnMouseMove(object sender, MouseEventArgs e)
{
    if (!(sender          is CartesianCustomAnnotation   ann) ||
        !(ann.DataContext is IProfileRegion             regVm) ||
        !(DataContext     is IProfileRoutine            rtnVm) ||
        e.StylusDevice != null)
    {
        return;
    }
 
    if (!ann.IsMouseCaptured || e.MouseDevice.LeftButton != MouseButtonState.Pressed)
        return;
 
    var pos     = e.GetPosition(Chart);
    var data    = Chart.ConvertPointToData(pos);
 
    // The data values we use depend on whether this is a region of the
    // X axis or of the Y axis.
 
    var newV = (double)data.FirstValue;
 
    // Keep the new value within limits of the data and of the other handle.
 
    if (ReferenceEquals(ann, ToHandle))
        ann.HorizontalValue  = Math.Min(Math.Max((double)FromHandle.HorizontalValue, newV), (double) HorizontalAxis.Maximum);
    else
        ann.HorizontalValue  = Math.Max(Math.Min((double)ToHandle.HorizontalValue, newV),   (double) HorizontalAxis.Minimum);
 
    e.Handled   = true;
}


I think if I were doing it over, I might still use annotations and an annotation provider but I might investigate moving them around with transparent windows Thumb controls.  I would likely need some sort of custom IMultiValueConverter to bind the thumb control dragging to the regions locations.  I could do in the converter all the calls that I'm currently doing in code-behind, such as "Chart.ConvertPointToData").  

But the fact is that this custom solution with code-behind works fairly well and we don't really have a need to make it any more generic.

Tags
ChartView
Asked by
Joe
Top achievements
Rank 2
Iron
Iron
Veteran
Answers by
Martin Ivanov
Telerik team
Joe
Top achievements
Rank 2
Iron
Iron
Veteran
2Plus
Top achievements
Rank 1
Share this question
or