Skip to main content Skip to footer

Adding More Interaction to C1Chart (Drag Points)

The ComponentOne Chart control (WPF/Silverlight) provides four useful methods that help with custom user interaction. They allow you to determine which data point is nearest the mouse, as well as quickly convert plot coordinates to control coordinates and vice versa. Use any combination of these methods to unlock virtually any form of user-to-plot interaction imaginable.

DataIndexFromPoint: Gets the index of closest data point that corresponds to the specified point.

DataIndexToPoint: Returns the point that corresponds to the specified data point. The data point is specified by its series and point indices.

PointFromData: Transforms the point from data coordinates to control coordinates.

PointToData: Transforms the point from control coordinates to data coordinates.

These methods are found on the ChartView object and can be accessed in code like: c1Chart1.View.DataIndexFromPoint(...). Now let's build an interactive sample which takes advantage of one of these methods: PointToData.

Sample Dragging Plotted Points

In general, charts are used to display data - not edit it. But in some scenarios you might want to enable the user to modify the data at run-time by dragging the points across the plot area.

To accomplish this task with C1Chart we need to modify the underlying data set and then have the chart reflect the changes. WPF (and Silverlight) has a special generic collection class ObservableCollection which provides notification about updating such as when items get added, removed, or when the entire list is refreshed. If an instance of this class is used as a data source for the chart, the C1Chart automatically reflects the changes that were made in the collection. So this task is very easy if we use an ObservableCollection as our data source. To get started you need to drop a C1Chart to your XAML page and set its ChartType to LineSymbolsSmoothed. This sample works best with only chart type that include symbols because they act as great adorners for dragging. For this sample I also set the Theme to DuskBlue. In code, add the System.Collections.ObjectModel namespace to your page (as well as C1.WPF.C1Chart). This includes the ObservableCollection.


using System.Collections.ObjectModel;  
using C1.WPF.C1Chart;  

Then declare an ObservableCollection of type Point. This will be our chart data source.


ObservableCollection<Point> points = new ObservableCollection<Point>();  

Clear all preset chart data (if some exists) and then fill the points collection with some dummy values.


//Clear chart data  
c1Chart1.Data.Children.Clear();  

//Create dummy data  
points.Add(new Point(0, 20));  
points.Add(new Point(1, 22));  
points.Add(new Point(2, 19));  
points.Add(new Point(3, 24));  
points.Add(new Point(4, 29));  
points.Add(new Point(5, 7));  
points.Add(new Point(6, 12));  
points.Add(new Point(7, 15));  

Next, create a XYDataSeries bound to this collection and add it to the chart.


//Setup C1Chart data series  
XYDataSeries ds = new XYDataSeries();  
ds.Label = "Series 1";  
ds.PlotElementLoaded += new EventHandler(ds_PlotElementLoaded);  
//Bind data series to collection  
ds.ItemsSource = points;  
//Important to set binding when using ItemsSource  
ds.ValueBinding = new Binding("Y");  
ds.XValueBinding = new Binding("X");  
//Add data series to chart  
c1Chart1.Data.Children.Add(ds);  

You can bind the collection of points directly to the ItemsSource of the data series. It's important to also specify the ValueBinding (Y) and XValueBinding to the X and Y fields of the Point object. Just like if this was your custom business object you'd have to bind the data series values to the desired field. Then add the data series to the chart's Data collection. You could easily add multiple data series following this approach. To capure our user interaction we need to handle these events on the chart.


c1Chart1.MouseMove += new MouseEventHandler(c1Chart1_MouseMove);  
c1Chart1.MouseLeftButtonUp += new MouseButtonEventHandler(pe_MouseLeftButtonUp);  

The PlotElementLoaded event fires on load for each plot element in the chart. Use this even to attach events for capturing the user clicking on the plotted element.


void ds_PlotElementLoaded(object sender, EventArgs e)  
{  
    var pe = (PlotElement)sender;  
    pe.MouseLeftButtonDown += new MouseButtonEventHandler(pe_MouseLeftButtonDown);  
    pe.MouseLeftButtonUp += new MouseButtonEventHandler(pe_MouseLeftButtonUp);  
}  

Next, when the user presses down on the mouse button we need to capture and save which plot element they are clicking. We save this in a PlotElement object. Notice we handle the MouseLeftButtonDown event ONLY on the plot element itself and not the chart, so there will always be a point under the cursor when this event fires. We handle the MouseLeftButtonUp to detach the selected point from the mouse (the user has let go of dragging). We set this event to fire for both the plot element AND the chart for a better user experience - incase the user drags really fast and the cursor slips off the PlotElement.


PlotElement currentPE;  
void pe_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)  
{  
    //Capture plot element clicked on  
    currentPE = (PlotElement)sender;  
}  

void pe_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)  
{  
    //Clear selected plot element  
    currentPE = null;  
}  

Finally, in the MouseMove event we take advantage of the PointToData method on C1Chart. This allows us to easily convert the mouse coordinates to data coordinates. We simply set this to be our plot point coordinates and the data point is updated as the user moves the mouse.


void c1Chart1_MouseMove(object sender, MouseEventArgs e)  
{  
    //If dragging a plot element  
    if (currentPE != null && currentPE.DataPoint.PointIndex != -1)  
    {  
        //Modify point value to match mouse position  
        points[currentPE.DataPoint.PointIndex] = c1Chart1.View.PointToData(e.GetPosition(c1Chart1));  
    }  
}  

Notice we are updating the data point directly in our collection. C1Chart automatically updates to reflect this change because we are bound to an ObservableCollection. We can determine the index of the point in our collection from the current selected PlotElement (currentPE.DataPoint.PointIndex). To only allow the user to drag points vertically we can restrict movement on the X plane. Add a checkbox to the page (chkRestrictX) and modify the c1Chart_MouseMove event as such:


void c1Chart1_MouseMove(object sender, MouseEventArgs e)  
{  
    //If dragging a plot element  
    if (currentPE != null && currentPE.DataPoint.PointIndex != -1)  
    {  
        //Modify point value to match mouse position  
        if (!(bool)chkRestrictX.IsChecked)  
        {  
            points[currentPE.DataPoint.PointIndex] = c1Chart1.View.PointToData(e.GetPosition(c1Chart1));  
        }  
        else  
        {  
            points[currentPE.DataPoint.PointIndex] = new Point(points[currentPE.DataPoint.PointIndex].X, c1Chart1.View.PointToData(e.GetPosition(c1Chart1)).Y);  
        }  
    }  
}  

Adding Tooltips

As nice finishing touch to this sample is to display the plot value in a tooltip as the user drags the points. This is achieved by designing a DataTemplate in XAML and then assigning it to the PointTooltipTemplate property on the DataSeries. Here is the data template:


<Window.Resources>  
    <DataTemplate x:Key="tooltip">  
        <StackPanel Orientation="Horizontal">  
            <TextBlock Text="{Binding Path=[XValues], Converter={x:Static c1chart:Converters.Format}, ConverterParameter=n2}" />  
            <TextBlock Text=", "></TextBlock>  
            <TextBlock Text="{Binding Path=Value, Converter={x:Static c1chart:Converters.Format}, ConverterParameter=n2}" />  
        </StackPanel>  
    </DataTemplate>  
</Window.Resources>  

We bind two TextBlocks to the X and Y values of the data point. Those scary looking converters are just to format the numeric value to just 2 decimal places. If we were declaring our DataSeries in XAML we could just assign our template to the PointTootipTemplate property on the data series with {StaticResource tooltip} notation, but since we clear the data and start fresh in code, we have to assign the DataTemplate in code as well. Add this line after you create the XYDataSeries ds:


ds.SetResourceReference(DataSeries.PointTooltipTemplateProperty, "tooltip");  

That's it! The choppiness of the image above is because this is a GIF. You have to build the sample to see for yourself how smooth it really is.

Download Sample Project

ComponentOne Product Manager Greg Lutz

Greg Lutz

comments powered by Disqus