Custom ChartModifiers - Part 5 - Select and Drag a Data-Point

Created by Lex Smith, Modified on Fri, 29 Mar at 1:12 AM by Lex Smith

The ChartModifier API


If you haven't read it already, please see our article on Custom ChartModifier API Overview. In this article, we introduce the basics of the ChartModifier API including an overview of this powerful base class. 


The ChartModifier API is by far the most powerful API in the SciChart library. Using this API you can create behaviours which you can attach to a chart to perform custom Zooming, Panning, Annotation & Markers, Legend output and much much more. Any time you want to do something in C# code to alter the behaviour of a SciChartSurface you should be thinking about creating a custom modifier to do it.


Custom ChartModifiers Part 5 - Selecting and Editing Data-Points


A lot of people ask us how you can click to select and drag a Data-Point on a SciChartSurface. This can also be done with the extremely powerful and flexible ChartModifier API. Below we show you how you can click-select a datapoint and drag it in the Y-direction in just 120 lines of code.



The SimpleDataPointEditModifier Source


The entire SimpleDataPointEditModifier class is included below. The code is fairly self-explanatory. Instead of inheriting ChartModifierBase, we inherit SeriesSelectionModifier, as this base-class handles the selection part very well.


using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using SciChart.Charting.ChartModifiers;
using SciChart.Charting.Model.DataSeries;
using SciChart.Charting.Visuals.RenderableSeries;
using SciChart.Core.Utility.Mouse;
 
namespace TestSuite.ExampleSandbox.CustomModifiers
{
    /// 
    /// Custom Modifier demonstrating selection of series and editing of data-points in the Y-Direction
    /// 
    public class SimpleDataPointEditModifier : SeriesSelectionModifier
    {
        private struct PointEditInfo
        {
            public HitTestInfo HitTestInfo;
            public int IndexOfDataSeries;
            public IDataSeries DataSeries;
        }
 
        private IRenderableSeries _selectedSeries;
        private Point _lastMousePoint;
        private PointEditInfo? _pointBeingEdited;
        private Ellipse _markerEllipse;
        private readonly double markerSize = 15;
 
        public SimpleDataPointEditModifier()
        {
            _markerEllipse = new Ellipse()
            {
                Width = markerSize,
                Height = markerSize,
                Fill = new SolidColorBrush(Colors.White),
                Opacity = 0.7,
            };
            Panel.SetZIndex(_markerEllipse, 9999);
        }
 
        public override void OnModifierMouseDown(ModifierMouseArgs e)
        {
            // Deliberately call OnModifierMouseUp in MouseDown handler to perform selection on mouse-down
            base.OnModifierMouseUp(e);
 
            // Get the selected series
            var selectedSeries = this.ParentSurface.RenderableSeries.FirstOrDefault(x => x.IsSelected);
 
            if (selectedSeries == null)
            {
                e.Handled = false;
                return;
            }
 
            // Perform a hit-test. Was the mouse over a data-point?
            var pointHitTestInfo = selectedSeries.HitTest(e.MousePoint, 10.0, false);
            if (pointHitTestInfo.IsHit)
            {
                // Store info about point selected
                int dataIndex = pointHitTestInfo.DataSeriesIndex; // Performs same functionality as selectedSeries.DataSeries.FindIndex(pointHitTestInfo.XValue);
                _pointBeingEdited = new PointEditInfo() { HitTestInfo = pointHitTestInfo, IndexOfDataSeries = dataIndex, DataSeries = selectedSeries.DataSeries };
 
                Canvas.SetLeft(_markerEllipse, pointHitTestInfo.HitTestPoint.X - markerSize * 0.5);
                Canvas.SetTop(_markerEllipse, pointHitTestInfo.HitTestPoint.Y - markerSize * 0.5);
                ModifierSurface.Children.Add(_markerEllipse);
            }
 
            _selectedSeries = selectedSeries;
            _lastMousePoint = e.MousePoint;
        }
 
        public override void OnModifierMouseMove(ModifierMouseArgs e)
        {
            base.OnModifierMouseMove(e);
 
            // Are we editing a data-point? 
            if (_pointBeingEdited.HasValue == false)
            {
                e.Handled = false;
                return;
            };
 
            var dataSeries = _pointBeingEdited.Value.DataSeries;
 
            // Compute how far we want to drag the point
            var currentMousePoint = e.MousePoint;
            double currentYValue = _selectedSeries.YAxis.GetCurrentCoordinateCalculator().GetDataValue(currentMousePoint.Y);
            double startYValue = _selectedSeries.YAxis.GetCurrentCoordinateCalculator().GetDataValue(_lastMousePoint.Y);
 
            var deltaY = currentYValue - startYValue;
 
            Canvas.SetLeft(_markerEllipse, _pointBeingEdited.Value.HitTestInfo.HitTestPoint.X - markerSize * 0.5);
            Canvas.SetTop(_markerEllipse, currentMousePoint.Y - markerSize * 0.5);
 
            // Offset the point
            OffsetPointAt(dataSeries, _pointBeingEdited.Value.IndexOfDataSeries, deltaY);
 
            _lastMousePoint = currentMousePoint;
        }
 
        public void OffsetPointAt(IDataSeries series, int index, double offset)
        {
            double yValue = ((double)series.YValues[index]);
            yValue += offset;
            series.YValues[index] = yValue;
 
            series.InvalidateParentSurface(RangeMode.None);
        }
 
        public override void OnModifierMouseUp(ModifierMouseArgs e)
        {
            _selectedSeries = null;
            _pointBeingEdited = null;
            ModifierSurface.Children.Remove(_markerEllipse);
            base.DeselectAll();
        }
    }
}


How does it work?


The full explanation of how it works is in the YouTube video above. If you can't see it (behind a corporate firewall), I encourage you to have a look from a mobile or personal device.


In short, the modifier inherits SeriesSelectionModifier which provides the base selection functionality. On mouse-down, the user selects a data-series, and we immediately perform a HitTest, to determine if the mouse-point is over a data-value. We determine this via the HitTestInfo.IsHit property in OnModifierMouseDown


Next, in OnModifierMouseMove, we take our previously hit data-point and offset it using the Coordinate Calculator API to determine the amount to offset our data-point by in the Y-Direction. 


Finally, the OffsetPointAt() method offsets a single data-point at the index we captured in OnModifierMouseDown


What else could you do?


We haven't covered all the possibilities for selecting and editing data-points with the mouse, but hopefully we've demonstrated what you can do with the ChartModifier API! It wouldn't be inconceivable to extend this code to select multiple data-points, or allow more complex editing of points. If you have any questions or suggestions on how we could improve this modifier and make it a core part of the SciChart library, do let us know.


Get the Custom ChartModifier Sandbox application


The link to the complete application with all our custom chart modifiers can be found in Part 1.



Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article