3

I want to be able to grab a datapoint drawn in a chart and to move it and change its position by dragging it over the chart control.

How can I ..

  1. ..grab the specific series point (series name ="My Series")
  2. When released the series point should change its position/ values

It's like making series points movable with drag event.

Here the color dots (points) should be able to move:

enter image description here

There are some charts like devExpress chart which perform this task but I want to do it in normal MS chart.

TaW
  • 53,122
  • 8
  • 69
  • 111
LittleThunder
  • 131
  • 1
  • 2
  • 12
  • Do you really want to move a datapoint (ie change its x- and/ or y-value) or do you want to move (i.e. scroll) the series? – TaW Apr 18 '16 at 18:16
  • @TaW : I have a series( of type point) with different points on it which is plot by a user using certain dialog input . So I want to move the datapoint so that the position of other point does not change only the position of the particular point that is Dragged should change. Also after the drag is over the point should fix in new position. Thanks for the reply I really need to do this. – LittleThunder Apr 19 '16 at 01:14
  • OK, I see. So the dragging should chnge both x- and y-values. Interesting idea! The 1st problem is in converting to and from pixels and data values. Can you tell me what the data values are ? DataType? - The 2nd the interactivity. I'll play around with it later today ;-) – TaW Apr 19 '16 at 04:36
  • the data types of the point are double . I will post a picture in my question to see what need to be dragged. Thanks – LittleThunder Apr 19 '16 at 10:18
  • So? The values can be double without changing the code! – TaW Apr 19 '16 at 10:39
  • I have had a look at your image. There should not be a problem with my solution. – TaW Apr 19 '16 at 11:27

2 Answers2

8

Moving a DataPoint is not a built-in feature of the Chart control. We need to code it..

The problem with interacting with a Chart by mouse is that there are not one but three coordinate systems at work in a Chart:

  • The chart elements, like a Legend or an Annotation are measured in percentages of the respective containers. Those data make up an ElementPosition and usually go from 0-100%.

  • The Mouse coordinates and all graphics drawn in one of the three Paint events, all work in pixels; they go from 0-Chart.ClientSize.Width/Height.

  • The DataPoints have an x-value and one (or more) y-values(s). Those are doubles and they can go from and to anywhere you set them to.

For our task we need to convert between mouse pixels and data values.

DO see the Update below!

enter image description hereenter image description here

There are several ways to do this, but I think this is the cleanest:

First we create a few class level variables that hold references to the targets:

// variables holding moveable parts:
ChartArea ca_ = null;
Series s_ = null;
DataPoint dp_ = null;
bool synched = false;

When we set up the chart we fill some of them:

ca_ = chart1.ChartAreas[0];
s_ = chart1.Series[0];

Next we need two helper functions. They do the 1st conversion between pixels and data values:

    // two helper functions:
    void SyncAllPoints(ChartArea ca, Series s)
    {
        foreach (DataPoint dp in s.Points) SyncAPoint(ca, s, dp);
        synched = true;
    }

    void SyncAPoint(ChartArea ca, Series s, DataPoint dp)
    {
        float mh = dp.MarkerSize / 2f;
        float px = (float)ca.AxisX.ValueToPixelPosition(dp.XValue);
        float py = (float)ca.AxisY.ValueToPixelPosition(dp.YValues[0]);
        dp.Tag = (new RectangleF(px - mh, py - mh, dp.MarkerSize, dp.MarkerSize));
    }

Note that I chose to use the Tag of each DataPoints to hold a RectangleF that has the clientRectangle of the DataPoint's Marker.

These rectangles will change whenever the chart is resized or other changes in the Layout, like sizing of a Legend etc.. have happend, so we need to re-synch them each time! And, of course you need to initially set them whenever you add a DataPoint!

Here is the Resize event:

private void chart1_Resize(object sender, EventArgs e)
{
    synched = false;
}

The actual refreshing of the rectangles is being triggered from the PrePaint event:

private void chart1_PrePaint(object sender, ChartPaintEventArgs e)
{
    if ( !synched) SyncAllPoints(ca_, s_);
}

Note that calling the ValueToPixelPosition is not always valid! If you call it at the wrong time it will return null.. We are calling it from the PrePaint event, which is fine. The flag will help keeping things efficient.

Now for the actual moving of a point: As usual we need to code the three mouse events:

In the MouseDown we loop over the Points collection until we find one with a Tag that contains the mouse position. Then we store it and change its Color..:

private void chart1_MouseDown(object sender, MouseEventArgs e)
{
    foreach (DataPoint dp in s_.Points)
        if (((RectangleF)dp.Tag).Contains(e.Location))
        {
            dp.Color = Color.Orange;
            dp_ = dp;
            break;
        }
}

In the MouseMove we do the reverse calculation and set the values of our point; note that we also synch its new position and trigger the Chart to refresh the display:

private void chart1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button.HasFlag(MouseButtons.Left) && dp_ != null)
    {
        float mh = dp_.MarkerSize / 2f;
        double vx = ca_.AxisX.PixelPositionToValue(e.Location.X);
        double vy = ca_.AxisY.PixelPositionToValue(e.Location.Y);

        dp_.SetValueXY(vx, vy);
        SyncAPoint(ca_, s_, dp_);
        chart1.Invalidate();
    }
   else
   {
       Cursor = Cursors.Default;
       foreach (DataPoint dp in s_.Points)
          if (((RectangleF)dp.Tag).Contains(e.Location))
          {
             Cursor = Cursors.Hand; break;
          }
   }
}

Finally we clean up in the MouseUp event:

    private void chart1_MouseUp(object sender, MouseEventArgs e)
    {
        if (dp_ != null)
        {
            dp_.Color = s_.Color;
            dp_ = null;
        }
    }

Here is how I have set up my chart:

Series S1 = chart1.Series[0];
ChartArea CA = chart1.ChartAreas[0];
S1.ChartType = SeriesChartType.Point;
S1.MarkerSize = 8;
S1.Points.AddXY(1, 1);
S1.Points.AddXY(2, 7);
S1.Points.AddXY(3, 2);
S1.Points.AddXY(4, 9);
S1.Points.AddXY(5, 19);
S1.Points.AddXY(6, 9);

S1.ToolTip = "(#VALX{0.##} / #VALY{0.##})";

S1.Color = Color.SeaGreen;

CA.AxisX.Minimum = S1.Points.Select(x => x.XValue).Min();
CA.AxisX.Maximum = S1.Points.Select(x => x.XValue).Max() + 1;
CA.AxisY.Minimum = S1.Points.Select(x => x.YValues[0]).Min();
CA.AxisY.Maximum = S1.Points.Select(x => x.YValues[0]).Max() + 1;
CA.AxisX.Interval = 1;
CA.AxisY.Interval = 1;

ca_ = chart1.ChartAreas[0];
s_ = chart1.Series[0];

Note that I have set both the Minima and Maxima as well as the Intervals for both Axes. This stops the Chart from running wild with its automatic display of Labels, GridLines, TickMarks etc..

Also note that this will work with any DataType for X- and YValues. Only the Tooltip formatting will have to be adapted..

Final note: To prevent the users from moving a DataPoint off the ChartArea you can add this check into the if-clause of the MouseMove event:

  RectangleF ippRect = InnerPlotPositionClientRectangle(chart1, ca_);
  if (!ippRect.Contains(e.Location) ) return;

For the InnerPlotPositionClientRectangle function see here!

Update:

On revisiting the code I wonder why I didn't choose a simpler way:

DataPoint curPoint = null;

private void chart1_MouseUp(object sender, MouseEventArgs e)
{
    curPoint = null;
}

private void chart1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button.HasFlag(MouseButtons.Left))
    {
        ChartArea ca = chart1.ChartAreas[0];
        Axis ax = ca.AxisX;
        Axis ay = ca.AxisY;

        HitTestResult hit = chart1.HitTest(e.X, e.Y);
        if (hit.PointIndex >= 0) curPoint = hit.Series.Points[hit.PointIndex];

        if (curPoint != null)
        {
            Series s = hit.Series;
            double dx = ax.PixelPositionToValue(e.X);
            double dy = ay.PixelPositionToValue(e.Y);

            curPoint.XValue = dx;
            curPoint.YValues[0] = dy;
        }
}
Community
  • 1
  • 1
TaW
  • 53,122
  • 8
  • 69
  • 111
  • If you are happy with the answer, please consider consider [accepting](http://stackoverflow.com/help/accepted-answer) it..! - I see that you have never done this: Go the the (invisible) checkmark at the top left, below the votes of the answer and click it! It turns green and gains us both a little reputation.. – TaW Apr 20 '16 at 10:59
  • Ok thanks I am new to stack overflow and I did not know that I will do it now . Just curious can we do the same solution using drag and drop event like dragEnter ,dragOver and dragLeave. Your solution works fine just curious to see if its possible. :) – LittleThunder Apr 20 '16 at 13:51
  • I don't think so. You could start it but can't really know which point you are one __and__ D&D will not really care about the precise position you drop, so you can't really put the point to a certain position either.. To add pixel precision to D&D wouln make it necessary to code the mouseevents anyway and it wouldn't be easy, since D&D basically filters them out. - The other options I mentioned would involve using `Annotations` but it would be more expensive plus I couldn't get it to work or doing the calculations differently but I didn't even try that since this solution works so nicely.. – TaW Apr 20 '16 at 14:40
  • yes had the same problem trying to do it with D&D as you mention .So I had to go with your solution. Thanks for the answer , and also thanks for answering my previous questions :) – LittleThunder Apr 20 '16 at 18:34
1

download Samples Environments for Microsoft Chart Controls

https://code.msdn.microsoft.com/Samples-Environments-for-b01e9c61

Check this:

Chart Features -> Interactive Charting -> Selection -> Changing Values by dragging

BHP
  • 443
  • 4
  • 13