3

I have some straight horizontal lines that I want the user be able to drag vertically. How would this be possible? I think the best parameter for line selection would be a fixed number of pixels near the line. So if mouse is +/- 2 pixels, I should change the mouse cursor and make the line drag-able.. I see the CurveItem class has properties IsSelectable and IsSelected. Will these have any function in solving this issue? I can’t really understand what they are for from reading the class documentation..


EDIT:

It seems that the FindNearestPoint (and FindNearestObject) only search actual points. How would I make selection to work along the whole section of a straight line? I guess I would need to make make my own custom "Find" routine that loops through all the lines I want to check, and for each calculate it's imaginary Y-point based on the mouse X position (?) I'm also thinking about sloped lines for this purpose, for horizontal/vertical lines it will be slightly simpler. At least it seems this is needed for a curveItem, but I assume the same must be done for selecting (at mid-section of) a LineObj?

I actually didn't know about the LineObj existed. It seems the LineObj is not possible to change the X2/Y2 coordinates, as they are ReadOnly. So is it at all possible to drag the X2/Y2 point of a LineObj?


EDIT 2:

It seems to be an issue with the FindNearestPoint on a JapaneseCandleStick graph; When I click in the graph pane, it does not return the index of the nearest bar, but I believe it instead selects the index with the closest Y-value, no matter how far away on the x axis it is. Sometimes it's a bar to the right of the mouse, sometimes to the left of the mouse. Is this the way it's meant to work?

I made this custom function myself, so I guess it's ok.. Still it would be nice to understand why the FindNearestPoint acts this way.

This is the code on mouseDown:

   ' Find nearest curve point:
   Dim ciNearestCurve As CurveItem
   Dim iNearestCurve As Integer
   Dim b As Boolean = zgc.GraphPane.FindNearestPoint(mousePt, zgc.GraphPane.CurveList, ciNearestCurve, iNearestCurve)
   If b Then
       With ciNearestCurve(iNearestCurve)
           Debug.Print(Date.FromOADate(.X) & " " & .Y & " " & .Z)
       End With
bretddog
  • 5,411
  • 11
  • 63
  • 111

3 Answers3

1

Take a look on this tutorial on dragging the points with mouse.

If you are using a LineObj instead of a curve, take a look on the FindNearestObject method.

Also if you want to make some "area of sensitivity" for clicking, this method should help you to transform mouse coordinates in pixels to pane scale coordinates.

The main idea is to:
- subscribe for MouseDown, MouseUp and MouseMove events
- in the handler for MouseDown event check if clicked point is near the curve/graph object you want to move
- do the change in similar way that it is shown in example from the first link

EDIT
Regarding your edit:
Let's assume you have a horizontal curve myCurve containing two points. Using FindNearestPoint you can find nearest clicked point and the curve containing this point.

So you have:

// mousePt is where you clicked
CurveItem nearestCurve = null;
int nearestID = -1;

myPane.FindNearestPoint(mousePt, out nearestCurve, out nearestID);
if(nearestCurve!=null)
   // remember the curve somewhere. 

Next handle the MouseMove and MouseUp events to find out how much you need to move your curve. You need to know only the change in Y (or Y2) direction as the curve is horizontal and you probably do not want to move it along X axis.

When you'll find out how much you need to move your curve (dy), just do:

for(int i=0; i<nearestCurve.Points.Count; i++)
    nearestCurve.Points[i].Y += dy;

Regarding your second question, in the documentation for LineObj.Location.Y2 you have:

Note that the Y2 position is stored internally as a Height offset from Y.

And Width/Height properties can be set easily, so you can do it this way.

Gacek
  • 10,184
  • 9
  • 54
  • 87
  • Thanks! Generally this looks manageable. But I'm unsure about a couple of things; Please see my edit above.. (not enough space here) – bretddog Oct 02 '10 at 09:51
  • 1
    Cheers! I can see this logic. But wouldn't this selection only trigger around the actual data-points on the LineItem or LineObj? I'm looking to extend this to make a line selectable at any chart-point along the "displayed" line, not only close to the actual (two) end-points that make up the line data. Would this require me to make a custom Find method which checks an imaginary interpolated point based on the end points for each line? Ah, Seems I didn't read in depth about LineObj..Y2. Now makes sense :) – bretddog Oct 02 '10 at 11:13
  • 1
    To make whole line selectable it is better to use LineObj - the `FindNearestObject` method checks the "area" of graphic object located on the pane, not only ending points. So I would strongly suggest to use `LineObj`. Even better, if you want the horizontal line be always present at the pane, from left ro right end of the chart, you can use (0,y) and (1, y) as the coordinates for this line and use `XChartFractionYscale` as `CoordinateFrame` – Gacek Oct 02 '10 at 12:12
  • Cool! very good you emphasized the LineObj.. I've now found out the basics of it, and it's of course very flexible. So now I made the drag work :) .... I have an issue with FindNearestPoint though, possibly, or maybe it's by design I'm not sure.. Described in "Edit 2" above.. – bretddog Oct 03 '10 at 06:56
  • AFAIR, `FindNearest...` doesn't work for bar objects. You need to perform reverse transormation of clicked point and then check the positions manually – Gacek Oct 04 '10 at 11:50
  • @Peter unfortunately the zedgraph.org website is down :( – Gacek May 04 '11 at 13:45
1

Firstly to answer to bretddog:

It seems to be an issue with the FindNearestPoint on a JapaneseCandleStick graph; When I click in the graph pane, it does not return the index of the nearest bar, but I believe it instead selects the index with the closest Y-value, no matter how far away on the x axis it is. Sometimes it's a bar to the right of the mouse, sometimes to the left of the mouse. Is this the way it's meant to work?

I made this custom function myself, so I guess it's ok.. Still it would be nice to understand why the FindNearestPoint acts this way

I don't work with JapaneseCandleStick but with Line, but I think it's the same kind of problem. ZedGraph works with coordinates, so with points, not with functions, so to determine the nearest "Curve" it should interpolate and it seems very hard to do that.

Nevertheless, for Line graphics I've develop a function to obtain the nearest curve. So I've made a straight line interpolation between each consecutive points for each curve, and I've used the mathematical distance to determine the nearest curve. The code is:

''' <summary>
''' To obtain the nearest curve and its index on ZedGraph stick
''' </summary>
''' <param name="GraphPane">The graphpane on wich you are working</param>
''' <param name="PointLocation">Mouse location</param>
''' <param name="NearestCurve">Reference of the nearest curve</param>
''' <param name="NearestCurveIndex">Index of the nearest curve</param>
''' <returns>True if a curve is found</returns>
''' <remarks></remarks>
Private Function FindNearestCurve(ByVal GraphPane As ZedGraph.GraphPane, ByVal PointLocation As System.Drawing.Point, ByRef NearestCurve As CurveItem, ByRef NearestCurveIndex As Integer) As Boolean
    Try
        Dim MinDist As Double = -1 'error if < 0
        Dim DistTemp As Double
        Dim a, b As Double
        Dim Curve As CurveItem
        Dim ValX, ValY As Double
        Dim NormX, NormY As Double

        'ini
        NearestCurveIndex = -1
        GraphPane.ReverseTransform(PointLocation, ValX, ValY) 'To use real values
        NormX = GraphPane.XAxis.Scale.Max - GraphPane.XAxis.Scale.Min 'To normalize value when we haven't orthonormal axis
        NormY = GraphPane.YAxis.Scale.Max - GraphPane.YAxis.Scale.Min 'To normalize value when we haven't orthonormal axis

        'We looking for the nearest curve
        For j = 0 To GraphPane.CurveList.Count - 1
            Curve = GraphPane.CurveList.Item(j)
            If Curve.IsVisible = True Then
                'We generate all coefficient (a and b) of straight line interpolation (equation y=ax+b)
                For i = 0 To Curve.NPts - 2 '-2 because we work on intervals
                    'we check if interval is close to the point (to prevent case where the complete interpolation curve is the nearest curve but the real segment is far to the point)
                    If (Curve.Points.Item(i + 1).Y >= ValY And Curve.Points.Item(i).Y <= ValY) Or
                            (Curve.Points.Item(i + 1).Y <= ValY And Curve.Points.Item(i).Y >= ValY) Or
                            (Curve.Points.Item(i + 1).X >= ValX And Curve.Points.Item(i).X <= ValX) Or
                            (Curve.Points.Item(i + 1).X <= ValX And Curve.Points.Item(i).X >= ValX) Then

                        'We calculate straight line interpolation coefficient a and b
                        'Vertical line case
                        If (Curve.Points.Item(i + 1).X / NormX - Curve.Points.Item(i).X / NormX) = 0 Then
                            'We calculate directly the distance
                            DistTemp = Math.Abs(Curve.Points.Item(i).X / NormX - ValX / NormX)
                        Else 'All other case
                            'a = (yi+1 - yi) / (xi+1 - xi)
                            a = (Curve.Points.Item(i + 1).Y / NormY - Curve.Points.Item(i).Y / NormY) / (Curve.Points.Item(i + 1).X / NormX - Curve.Points.Item(i).X / NormX)
                            'b = yi - a*xi
                            b = Curve.Points.Item(i).Y / NormY - a * Curve.Points.Item(i).X / NormX
                            'We calculate the minimum distance between the point and all straight line interpolation
                            DistTemp = Math.Abs(a * ValX / NormX - ValY / NormY + b) / Math.Sqrt(1 + a * a)
                        End If
                        'We test if it's the minimum and save corresponding curve
                        If MinDist = -1 Then
                            MinDist = DistTemp 'first time
                            NearestCurveIndex = j
                        ElseIf DistTemp < MinDist Then
                            MinDist = DistTemp
                            NearestCurveIndex = j
                        End If
                    End If
                Next
            End If
        Next

        'Return the result
        If NearestCurveIndex >= 0 And NearestCurveIndex < GraphPane.CurveList.Count Then
            NearestCurve = GraphPane.CurveList.Item(NearestCurveIndex)
            Return True
        Else
            NearestCurve = Nothing
            NearestCurveIndex = -1
            Return False
        End If

    Catch ex As Exception
        NearestCurve = Nothing
        NearestCurveIndex = -1
        Return False
    End Try
End Function

I've tested this function, and it seems to work well, but I can't guarantee in all cases (indeed, if the first/last point of a curve is the nearest point, it won't be detected as such). Some remarks about using:

  • Work only on visible curve, to remove it, remove If Curve.IsVisible = True Then line ;
  • I've normalized X,Y values before the calculation to prevent disagreement with a non-orthonormal axis;
  • When there is an error, return False and you have NearestCurve = Nothing and NearestCurveIndex = -1;
  • The line cursor that you want to drag should be a curve (with to points or more, no matter), not a LineObj;
  • The test if the interval is close or not is the weak part of the code, and on which I suppose it should happen some mistakes (I've already identified one - rare - case, as said before). A problem can also appear if you have a not perfect vertical line (so with a very big a coefficient).

Lastly, I'm not sure that this code is optimized for speed, but I've no freezing on my side. The best way should be to integrate the function to a ZedGraph class, and calculate each coefficient (a and b) when Add function is called, so to not calculate them each time (so each mouse move).

So I hope that code will help some people to create a movable cursor, something which is very missing in ZedGraph.

Community
  • 1
  • 1
Alex
  • 11
  • 2
1

I needed to drag a line object on a plot. It took me quite some time to work out how to do it. The following code is specific to my application and not complete but it does work and I think its a good starting point for anyone else who needs to do this. My code is in VB. The essence of it is to use the MouseDownEvent to determine if the cursor is close enough to the object you want to drag. And then in the MouseMoveEvent determine the new location and update the plot.

Private Function zgPlot_MouseDown(sender As ZedGraphControl, e As MouseEventArgs) As Boolean Handles zgPlot.MouseDownEvent

    'Return true if you have handled the mouse event entirely, and you do not want the ZedGraphControl to do any further action (e.g., starting a zoom operation). 
    'Return False If ZedGraph should go ahead And process the mouse Event.
    mbDraggingThresholdLine = False
    mbThresholdHasChanged = False

    If e.Button = MouseButtons.Right Then
        'this was a right click for the context menu, we dont want to process it here
        Return False
    End If

    Dim ptClicked As New Point(e.X, e.Y)
    Dim dblX_ClickedScaleValue As Double
    Dim dblY_ClickedScaleValue As Double

    'this function passes in the mouse point and gets back the graph pane x and y scale value
    'In this case I only care about the Y value
    zgPlot.GraphPane.ReverseTransform(ptClicked, dblX_ClickedScaleValue, dblY_ClickedScaleValue)

    If Me.mcTrack.mbChangeThresholdIsValid = True Then
        'this plot has a threshold line, if it doesnt have a threshold line then there is nothing to do

        'find out if the mouse down event is close enough to the threshold line to consider the threshold line as draggable
        If Math.Abs(Me.mcTrack.mdYaxisThresholdPlotValue - dblY_ClickedScaleValue) < 1 Then
            'the mouse down event occured within 1 of the threshold line
            mbDraggingThresholdLine = True 'set flag to be used during the MouseMoveEvent to see if a drag is in progress
            mdOriginalThresholdValue = Me.mcTrack.mdYaxisThresholdPlotValue 'save the original Y value of the line
            Return True
        End If

    End If

    Return False

End Function

Private Function zgPlot_MouseMove(sender As ZedGraphControl, e As MouseEventArgs) As Boolean Handles zgPlot.MouseMoveEvent

    If mbDraggingThresholdLine = True Then
        'we are dragging the threshold line

        'get the latest values under the cursor
        Dim ptClicked As New Point(e.X, e.Y)
        Dim dblX_ClickedScaleValue As Double
        Dim dblY_ClickedScaleValue As Double
        zgPlot.GraphPane.ReverseTransform(ptClicked, dblX_ClickedScaleValue, dblY_ClickedScaleValue)
        mdNewThresholdValue = dblY_ClickedScaleValue
        mbThresholdHasChanged = True

        'do an update of the plot objects using the new cursor value
        'the following just moves the threshold on this one plot, without changing the underlying protocol defined value of the threshold and without changing all the other plots using it.
        Me.mcTrack.mdYaxisThresholdPlotValue = mdNewThresholdValue
        Me.doPlot()
        Return True
    End If

    Return False
End Function