3
$\begingroup$

I need to have a mouse cursor that identifies points that are close to the mouse position on a graphics plot. The difficulty is that the scaling of the plot makes this difficult. I think I am missing something elementary.

Here are some points a plot range and an aspect ratio. I also give a simple mouse definition and a dynamic module showing the points.

SeedRandom[123];
pts = Table[{RandomReal[{0, 0.1}], RandomReal[{0, 10}]}, 20];
pRange = {{0, 0.1}, {0, 10}}; (* Plot range *)
ar = 1/4; (* Aspect Ratio *)

    ClearAll[mouse];
mouse[pts_] := Module[{p, n},
  p = MousePosition[{"Graphics", Graphics}, {}];
  {n} = Nearest[pts -> "Index", p];
  If[NumberQ[n], {pts[[n]], n}, {{}, {}}]
  ]

DynamicModule[{},
 Column[{
   Dynamic[mouse[pts]],
   Dynamic[Graphics[{PointSize[Medium], Point[pts],
      Red, Point[mouse[pts][[1]]]},
     Frame -> True, AspectRatio -> ar, PlotRange -> pRange, 
     ImageSize -> 10 72]]

Failed first attempt

The red dot and the mouse are far apart because of the distortion due to the axes scaling and the aspect ratio. This is therefore no good. On a secondary point I also need suggestions for a better approach for when the mouse is out of the graphics area.

I therefore defined a distance function that takes into account the axis scaling and the aspect ratio. Here is the second attempt:

ClearAll[mouse];
mouse[pts_, {{x1_, x2_}, {y1_, y2_}} _ : {{0, 1}, {0, 1}}, ar_ : 1] :=
  Module[{p, n},
  p = MousePosition[{"Graphics", Graphics}, {}];
  {n} = Nearest[pts -> "Index", p, 
    DistanceFunction :> (Sqrt[((#1[[1]] - #2[[1]])/(
         x2 - x1))^2 + (ar ((#1[[2]] - #2[[2]])/(y2 - y1)))^2] &)];
  If[IntegerQ[n], {pts[[n]], n}, {{}, {}}]
  ]

DynamicModule[{},
 Column[{
   Dynamic[mouse[pts, pRange, ar]],
   Dynamic[Graphics[{PointSize[Medium], Point[pts],
      Red, Point[mouse[pts, pRange, ar][[1]]]},
     Frame -> True, AspectRatio -> ar, PlotRange -> pRange, 
     ImageSize -> 10 72]]
   }]
 ]

Second attempt

This works better and identifies the point nearest the scaled distance from the mouse. However, I need to go further, I need to be able to select points that are within a certain distance of the mouse. So the distance function needs to define a distance that is, for example, within a circle of radius equal to 0.1 of the x-axis length. So I define a distance function that has an If statement to sort close and near points. Here is where it goes wrong for me. This is the new mouse function and DynamicModule

ClearAll[mouse];
mouse[pts_, {{x1_, x2_}, {y1_, y2_}} _ : {{0, 1}, {0, 1}}, ar_ : 1, 
  r_ : ∞] := Module[{p, n, dist},
  p = MousePosition[{"Graphics", Graphics}, {}];
  {n} = Nearest[pts -> "Index", p, 
    DistanceFunction :> 
     If[dist = (Sqrt[((#1[[1]] - #2[[1]])/(
           x2 - x1))^2 + (ar ((#1[[2]] - #2[[2]])/(y2 - y1)))^2] &); 
      dist < r (x2 - x1), dist, ∞]];
  If[IntegerQ[n], {pts[[n]], n}, {{}, {}}]
  ]


DynamicModule[{},
 Column[{
   Dynamic[mouse[pts, pRange, ar, 0.1]],
   Dynamic[Graphics[{PointSize[Medium], Point[pts],
      Red, Point[mouse[pts, pRange, ar, 0.1][[1]]]},
     Frame -> True, AspectRatio -> ar, PlotRange -> pRange, 
     ImageSize -> 10 72]]
   }]
 ]

Not working distance function

The distance function has gone wrong. I think this may be a difficulty with an If statement in a pure function, however I am not sure and am very deep in uncertainty. Any suggestions? Also, is there a built-in function that does what I want already? Thanks

$\endgroup$
2
  • $\begingroup$ Do you think you could reduce the complexity of your code, perhaps limiting yourself to one attempted implementation, and to a more minimal example? I found it confusing to pinpoint the problem; also, some of the code may be missing a piece because I was unable to run it. See my answer below for a hint of what I understood so far. $\endgroup$ – MarcoB May 30 '20 at 21:58
  • $\begingroup$ Hugh, please add your solution as an answer, not as part of the question. You can then edit the question to refer to it. Then, if a new issue is raised, I think it would be best to ask another question starting from your new solution as a starting point, $\endgroup$ – MarcoB May 31 '20 at 16:05
4
$\begingroup$

With this you'll get the correct visually nearest point instead of the distortions you were experiencing.

SeedRandom[123];
pts = Table[{RandomReal[{0, 0.1}], RandomReal[{0, 10}]}, 20];
plotrange = {{0, 0.1}, {0, 10}};
aspectratio = 1/4;
transform = 
  RescalingTransform[plotrange, {{0, 1/aspectratio}, {0, 1}}];
scaledpts = transform /@ pts;
nf = Nearest[scaledpts -> "Index"];
Dynamic[
 Module[{p = MousePosition[{"Graphics", Graphics}, {0, 0}], n},
  n = pts[[nf[transform[p]][[1]]]];
  Column[{p,
    Graphics[{Line[{p, n}], PointSize[Medium], Point[pts], Red, 
      PointSize[Large], Point[n]}, Frame -> True, 
     AspectRatio -> aspectratio, PlotRange -> plotrange, 
     PlotRangePadding -> 0, ImageSize -> 10 72]}]]]

nearest point visual

$\endgroup$
5
  • $\begingroup$ Thank you for this. I particularly like the way you have defined functions for nearest function and aspect ratio transform. I will learn from this. This is more or less what I am looking for. I tried to add a function that would not connect to any point unless within a defined distance. Here you have transformed both the points and the mouse position. My idea was to do just one scaling based on a DistanceFunction supplied to Nearest. Do you see an advantage in scaling the points and the mouse position separately? $\endgroup$ – Hugh May 31 '20 at 8:12
  • $\begingroup$ You cannot do Nearest[pts] as you get distortions - you must rescale the points first - and the mouse position needs to transform into the same space before using the NearestFunction on it. $\endgroup$ – flinty May 31 '20 at 13:18
  • $\begingroup$ In maths speak the reason is like this under the $L_p$ norm and given a matrix norm $|| \mathbf{M} \vec{v} ||_p \ne | \mathbf{M} | \cdot||\vec{v}||_p$ $\endgroup$ – flinty May 31 '20 at 13:28
  • $\begingroup$ I have put my solution which builds on yours . However, I have managed to use a DistanceFunction that I use with Nearest. This re-scales both the mouse position and the points being considered simultaneously.. What software did you use to make your animation? I would like to follow your example? $\endgroup$ – Hugh May 31 '20 at 15:34
  • $\begingroup$ I used ScreenToGif screentogif.com $\endgroup$ – flinty May 31 '20 at 15:38
2
$\begingroup$

Here is the solution I have come to which is built on the answers below.

SeedRandom[123];
pts = Table[{RandomReal[{0, 0.1}], RandomReal[{0, 10}]}, 20];
pRange = {{0, 0.1}, {0, 10}};
ar = 1/4;

ClearAll[distF];
distF[{{x1_, x2_}, {y1_, y2_}}, ar_][p1_, p2_] := Module[{ },
  Sqrt[((p1[[1]] - p2[[1]])/(
    x2 - x1))^2 + (ar (p1[[2]] - p2[[2]])/(y2 - y1))^2]
  ]

ClearAll[npt];
npt::usage = 
  "npt[points, position] returns the point and point number of the  \
nearest point in pts to p. Output is {nearest pt, n}. If p = {} then \
output is {{},None} ";
npt[pts_, p_, r_] := Module[{n, dist},
  If[p == {}, Return[{{}, None}]];
  {n} = Nearest[pts -> "Index", p, 
    DistanceFunction -> distF[pRange, ar]];
  dist = distF[pRange, ar][pts[[n]], p];
  If[dist < r, {pts[[n]], n}, {{}, None}]
  ]
ClearAll[mp];
mp[] := MousePosition[{"Graphics", Graphics}, {}];

DynamicModule[{pt, n},
 Column[{
   Dynamic[{pt, n} = npt[pts, mp[], 0.05]],
   Dynamic@Graphics[{
      PointSize[Medium], Point[pts],
      Red, PointSize[Large], Point[pt]
      },
     Frame -> True, AspectRatio -> ar, PlotRange -> pRange, 
     PlotRangePadding -> 0, ImageSize -> 10 72]
   }]
 ]

Gif showing operating module

This works as I want with the selected point being identified as the mouse comes close. What I have done is to make a distance function distF that I then supply to Nearest

$\endgroup$
0
$\begingroup$

Unfortunately I found it quite difficult to follow your code approaches, also because the first snipped is missing some braces / does not execute.

Here is my approach to what I understand of your problem:

SeedRandom[123]
data = RandomReal[{-10, 10}, {40, 2}];

rnf = First@*Nearest[data];

ListPlot[
 data,
 PlotRange -> All, Frame -> True, Axes -> False,
 AspectRatio -> 1,
 Epilog -> DynamicModule[
    {pos},
    pos = Dynamic[rnf@MousePosition["Graphics", {0, 0}]];
    {Red, PointSize[0.02], Point[pos],
     Inset[
       Style[Round[#, 0.01] & /@ #, 14],
       # + 0.5
     ]& @ pos
    }
  ]
]

animated gif of the mouse moving and the highlighted point following it

$\endgroup$
2
  • $\begingroup$ Thank you for this. The problem occurs when you have very different axes scaling and you plot with an aspect ratio other than 1. Then the nearest on the basis of screen distance is very different to the nearest on the basis of coordinate difference. Some scaling is needed; the dilemma is: should one scale the distance function or the coordinates of the points? I was trying to scale the distance function used by Nearest. $\endgroup$ – Hugh May 31 '20 at 7:44
  • $\begingroup$ @Hugh That makes more sense. Thank you for the clarification! $\endgroup$ – MarcoB May 31 '20 at 16:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.