Take the 2-minute tour ×
Mathematica Stack Exchange is a question and answer site for users of Mathematica. It's 100% free, no registration required.

When it comes to visual analysis, large datasets or data with intricate internal details often makes plotting in 2D useless, as the outcome is either just a fraction of the full dataset, or no details can be observed in the mess of datapoints. How can one make the process of changing the plot range and/or zooming, panning, etc. less tedious than doing it programmatically and iteratively, from time to time? I often meet with this issue, and developed various methods to deal with it (like this). Though I have now a working solution that I would like to share (see below), I also am highly interested in what kind of methods and tricks others invented to visualize and manipulate complex 2D data with ease.

share|improve this question
add comment

2 Answers

up vote 66 down vote accepted

Since Mathematica does not have a built-in plot manipulating interface, here is a gui of a plot-manipulator. Updates are indicated with bold text.

Functionality:

  • Should work with any plot/graphics (ArrayPlot compatibility added);
  • Drag anywhere in plot zooms in to selected rectangle; can be done repeatedly;
  • Ctrl+drag zooms in/out (along vertical axis);
  • Shift+drag pans;
  • Vertical and horizontal sliders appear along the bottom and left edges when mouse is over:

    • dragging sets the scale of the range along the manipulated axis that is displayed;
    • Shift+dragging pans the display area along the manipulated axis;
  • Double click anywhere resets the plot.
  • Task-specific mouse cursors.
  • Replot button in upper right corner (appears when mouse is over). Only applicable for iterator-based plots (like Plot, DensityPlot, StreamPlot, etc.). Works only when the plot is passed unevaluated to PlotExplorer (i.e. as Plot[ ...]) instead of a variable like plot, as in the latter case the iterator structure is lost, and plot type (and thus new iterator value (s)) cannot be deduced.
  • Compatible with Manipulate, DynamicModule and like, though updating due to interacting with the outer controllers (of e.g. Manipulate) is rather slow.
  • Pressing Alt while the mouse is moving displays the current coordinates where mouse points to.
  • No explicit ImageSize has to be supplied with the plot anymore. Any specification (All, Automatic, Tiny, etc. and their combinations) suffices thanks to Szabolcs.

Known issues:

  • Resizing mouse cursor does not appear when dragging zoom-rectangle.
  • Since sampling frequency cannot be changed from the GUI, a zoomed-out plot might look really bad, even when replot. Increase e.g. PlotPloints in the plot function call.
  • When hitting the replot button after e.g. zooming in and then double-clicking to restore original view the plot appears cut as it now only displayes the small area that was replot when zoomed in. Hitting the replot button again solves this issue.
  • Replotting a non-deterministic plot might not produce the same as the original.
  • DiscretePlot, ParametricPlot, PolarPlot cannot be replotted (i.e. left intact) as the iterator values for these functions cannot be deduced from plot ranges.
  • Fixed a bug with an off-graphics MousePosition value being non-numeric.

TODO:

  • Display mouse coordinates in status area. This is harder than I thought, as any dynamic display of the coordinates requires a Dynamic wrapper outside the StatusArea wrapper enclosing the displayed plot, which interferes with all the dynamic controls of the plot.

Sources:

  • Zooming rectangle by Szabolcs (here)
  • Detecting double clicks by kguler (here)
  • "MouseEntered" & "MouseExited" by Mike Honeychurch & kguler (here)
  • Panning by Heike (here)

The function:

Attributes[PlotExplorer] = {HoldFirst};
PlotExplorer[defPlot_] := DynamicModule[{
  held, head, plot, range, zoomRange, defRange, defSize, defEpilog, size,
  pt1, pt2, spt1, dist, rect = {},
  dragged = False, overButton = False, pushedButton = False, 
  overHor = False, overVer = False, activeHor = False, activeVer = False,
  xPos = .5, yPos = .5, minDistance = 0.1, sliderRatio = 0.05, panFactor = 10,
  reset, distance, slider, replot, coord = None
  },

 held = Hold@defPlot;
 head = Part[held, 1, 0];

 plot = defPlot;
 {defEpilog, defRange} = {Epilog, PlotRange} /. Quiet@AbsoluteOptions@plot;
 defEpilog = defEpilog /. Epilog -> {};
 size = defSize = Rasterize[plot, "RasterSize"];
 range = zoomRange = defRange;

 Deploy@EventHandler[Dynamic[MouseAppearance[
   Show[plot,
    PlotRange -> Dynamic@range,
    Epilog -> {
      defEpilog, (* Original epilog *)

      (* Replot button *)
      EventHandler[
       Inset[Graphics[{
           Black, Dynamic@Opacity@If[overButton, If[pushedButton, .2, .1], 0], 
           Rectangle[{0, 0}, {1, 1}, RoundingRadius -> Offset@5], 
           Black, Dynamic@Opacity@If[overButton, If[pushedButton, .9, .7], 0], 
           Text["Replot", Center]}, ImageSize -> 50, 
         AspectRatio -> 1/2, 
         BaseStyle -> {FontFamily -> "Helvetica"}],
       Scaled@{1, 1}, Scaled@{1, 1}],
      {
       "MouseEntered" :> (overButton = True), 
       "MouseExited" :> (overButton = False), 
       "MouseDown" :> (pushedButton = True), 
       "MouseUp" :> (pushedButton = False; replot@plot)
       }],

      (* Zoom rectangle *)
      FaceForm@{Blue, Opacity@.1}, 
      EdgeForm@{Thin, Dotted, Opacity@.5}, Dynamic@rect,

      (* Range sliders *)
      Inset[slider[Dynamic@xPos, Dynamic@overHor, 
            Dynamic@activeHor, size, True], Scaled@{.5, 0}, Scaled@{.5, 0}],
      Inset[slider[Dynamic@yPos, Dynamic@overVer,
            Dynamic@activeVer, size, False], Scaled@{0, .5}, Scaled@{0, .5}]
      }],

   (* cursor *)
   Which[
    dragged, "FrameRisingResize",
    CurrentValue@"AltKey", Graphics[{
        Antialiasing -> False, Line@{{-1, 0}, {1, 0}}, Line@{{0, -1}, {0, 1}},
        Dynamic@Text[Style[coord, FontFamily->"Helvetica", 8], {0, 0}, {-1.1, -1}]},
        ImageSize -> 160, ImagePadding -> 0, AspectRatio -> 1, 
        PlotRange -> {{-1, 1}, {-1, 1}}*8],
    CurrentValue@"ControlKey" && !overHor && !overVer, "ZoomView",
    CurrentValue@"ShiftKey" && !overHor && !overVer, "DragGraphics",
    CurrentValue@"ShiftKey" && (overHor || overVer), "LinkHand",
    True, Automatic
    ]
   ], TrackedSymbols :> {dragged, plot}],
 {
  "MouseMoved" :> (coord = MousePosition@"Graphics"),
  "MouseClicked" :> If[!overButton && CurrentValue@"MouseClickCount" == 2, reset[]],
  "MouseDown" :> If[(! overHor || !overVer), pt1 = MousePosition@"Graphics";
                 spt1 = MousePosition@"GraphicsScaled"],
  "MouseDragged" :> If[(!overHor && !overVer || dragged) &&
       !overButton && !pushedButton && !activeVer && !activeHor, dragged = True;
    Which[
     CurrentValue@"ControlKey", 
       dist = Last@MousePosition@"GraphicsScaled" - Last@spt1; 
       range = zoomRange*10^dist,
       (* "GraphicsScaled" is required below as "Graphics" would give
          jumpy results as the plot range is constantly modified. *)
     CurrentValue@"ShiftKey", 
       pt2 = MapThread[Rescale, {MousePosition@"GraphicsScaled",
                                 {{0, 1}, {0, 1}}, zoomRange}] - pt1; 
       range = zoomRange - pt2,
     True, pt2 = MousePosition@"Graphics"; 
     rect = If[pt2 === None, {}, Rectangle[pt1, pt2]]
     ]],
  "MouseUp" :> (If[!CurrentValue@"ShiftKey" && !CurrentValue@"ControlKey"
            && dragged && distance[pt1, pt2, zoomRange] > minDistance, rect = {}; 
     range = Transpose@Sort@{pt1, pt2};]; zoomRange = range; 
     dragged = False)
  },
 PassEventsDown -> True, PassEventsUp -> True
 ],

Initialization :> (

 slider[Dynamic[pos_], Dynamic[over_], Dynamic[active_], size_, hor_] := EventHandler[
 Dynamic[LocatorPane[
   If[hor, Dynamic[{pos, .5}, (
       pos = Clip[First@#, {0, 1}]; 
       range[[1]] = If[CurrentValue@"ShiftKey",
         First@zoomRange + (pos - .5)*Abs[Subtract @@ First@zoomRange]*panFactor,
         First@zoomRange*10^((pos - .5)*2)
         ]) &],
    Dynamic[{.5, pos}, (
       pos = Clip[Last@#, {0, 1}]; 
       range[[2]] = If[CurrentValue@"ShiftKey",
         Last@zoomRange + (pos - .5)*Abs[Subtract @@ Last@zoomRange]*panFactor,
         Last@zoomRange*10^((pos - .5)*2)
         ]) &]],
   (* Locator below is required inside Graphics as LocatorPane inside
      Inset looses the crosshair image. *)
   Graphics[{
      Black, Opacity@If[(over || active) && ! dragged, .1, 0], 
      Rectangle[{0, 0}, {1, 1}, RoundingRadius -> Offset@5], 
      Opacity@1, Locator[Dynamic@If[hor, {pos, .5}, {.5, pos}],
          Appearance -> If[(over || active) && ! dragged, Automatic, None]]
     },
    ImagePadding -> 0, PlotRangePadding -> 0, 
    ImageSize -> If[hor, #*{1, 0} + {0, 20} &, #*{0, 1} + {20, 0} &]@size, 
    AspectRatio -> If[hor, 1/sliderRatio/First@size, sliderRatio*Last@size]
   ],
   Enabled -> (over || active),
   Appearance -> None,
   FrameMargins -> 0,
   ContentPadding -> False
  ],
  TrackedSymbols :> {pos, over, active, dragged}],
 {
  "MouseDown" :> (active = True),
  "MouseUp" :> (active = False),
  "MouseEntered" :> (over = ! If[hor, activeVer, activeHor]),
  (* The value of `active` must be public so that one slider can check whether
     the other is dragged, not to pop up if the other locator is moved over
     this slider's area. *)
  "MouseExited" :> (over = False)
  }, PassEventsDown -> True];

replot[p_] := Module[{temp}, Switch[head,
  Plot,
  temp = ReplacePart[held, 
         {{1, 2} -> {held[[1, 2, 1]], range[[1, 1]], range[[1, 2]]}}];
  (* Replace plot range or insert if nonexistent *)
  plot = ReleaseHold@If[MemberQ[temp, PlotRange, Infinity], 
     temp /. {_[PlotRange, _] -> (PlotRange -> range)}, 
     Insert[temp, PlotRange -> range, {1, -1}]],

  DensityPlot | ContourPlot | RegionPlot | StreamPlot | StreamDensityPlot |
                VectorPlot | VectorDensityPlot | LineIntegralConvolutionPlot,
  temp = ReplacePart[held, {{1, 2} -> {held[[1, 2, 1]], range[[1, 1]], 
       range[[1, 2]]}, {1, 3} -> {held[[1, 3, 1]], range[[2, 1]], range[[2, 2]]}}];
  plot = ReleaseHold@If[MemberQ[temp, PlotRange, Infinity], 
     temp /. {_[PlotRange, _] -> (PlotRange -> range)},
     Insert[temp, PlotRange -> range, {1, -1}]];,

  _, Null
  ]];

reset[] := (range = zoomRange = defRange; xPos = yPos = .5);

(* Quiet suppresses error message when Rescale[y, {x, x}] where y != x. 
   EuclideanDistance then defaults to Infinity which is fine. *)
 distance[p1_, p2_, {r1_, r2_}] := Quiet[
    N@EuclideanDistance[Rescale[p1, r1], Rescale[p2, r2]]];
)
];

Now let's call it with some plots:

PlotExplorer@Plot[Evaluate@Table[BesselJ[n, x/50]*4, {n, 4}], {x, -10000, 10000},
   Filling -> Axis, Epilog -> {Red, Text[Style["Epilog text", Bold, 24], {600, .2}]},
   PlotRange -> {{0, 1000}, {-1, 1}}, ImageSize -> {400, 300}]

enter image description here

Zooming in/out with the Ctrl key, panning with Shift using the slider:

enter image description here

PlotExplorer@ContourPlot[Sin[x y], {x, 0, 3}, {y, 0, 3}, 
   ColorFunction -> "BlueGreenYellow", PlotPoints -> 25, 
   ImageSize -> {300, 300}]

enter image description here

PlotExplorer@ArrayPlot[RandomReal[1, {10, 20}], ColorFunction -> "Pastel", 
   ImageSize -> {400, 200}, AspectRatio -> 1/2, 
   PlotRange -> {{1, 10}, {1, 20}}]

Panning the plotted area using Shift:

enter image description here

Please report any errors/glitches if encountered, and please feel free to correct it in this post!

share|improve this answer
    
This is great, +1! Now we need to extend it to allow for a replotting event in all cases possible –  Rojo Jun 21 '12 at 3:21
    
How can I get coordinates –  P. Fonseca Jun 21 '12 at 6:25
1  
@Rojo, P.Fonseca: Replotting is indeed important, as the $y$-scale is usually cut off if zoomed out. I try to incorporate more functionality over time, coordinates are on the todo list. If you have other suggestions, please voice them! –  István Zachar Jun 21 '12 at 9:39
    
+1 Magnificent! I can think of so many situations where this will be useful. –  David Carraher Jun 21 '12 at 11:51
1  
Is there any way to integrate this with Manipulate (or something similar) to change the parameters of the plotted function? That would be a very handy addition and would make it fully interactive. –  sebhofer Jul 13 '12 at 12:41
show 15 more comments

In addition to István's fine answer, there is also Experimental`Explore[] which provides almost all the functionalities in his PlotExplorer. I think it was Szabolcs who first told me of this function.

If you call the above function with no arguments, you can choose to interactively work with either Plot, ParametricPlot, Manipulate or Graphics. Alternately, you can call it with either of these as an argument. Choosing Plot gives you a window like this:

enter image description here

Now you can simply enter the different functions that you're plotting and you can manipulate it using simple dropdown menus and checkboxes. Some of the features are

  • Edit all plot/axes/frame/etc. styles interactively
  • Real time update of changes
  • Pan the plot by clicking and dragging
  • Zoom the plot by pressing Option and dragging on a Mac (most likely Alt drag for Windows/Linux users)
  • Set the desired image size by dragging the plot
  • Add annotations which are inserted as Epilogs
  • Calculate the slope and value of the functions at any given x
  • Copy the corresponding Mathematica code for use in notebooks.

Here's an example of an explorer panel with lot of customizations done to the plot:

enter image description here

One caveat though — the fact that it is in Experimental` might give a hint that it is undocumented, possibly unstable and subject to change at any time without notice. Nevertheless, it is a very useful tool for those that do not wish to fiddle with their plots programmatically.

share|improve this answer
    
Thanks for adding this detailed description, I really hoped someone will do this! Adding to the caveat: I remember crashing the explorer many times by simply fiddling with the options/directives so I assume it is not part of the official canon because of these issues. Hopefully it will be integrated in version 9. –  István Zachar Jun 26 '12 at 9:39
1  
When I try this in version 9, the plot button and the graphics button don't work ("Experimental`Explore::notmpl: Plot type exploration of Null is not currently supported." for both), ParametricPlot returns a window but also an error. And this is without even trying any of the features, this is just from clicking the buttons. –  Pickett Apr 3 '13 at 8:57
    
@Anon Ah well, that's why it's in the Experimental` context... no guarantees, whatsoever :) I don't think it was broken deliberately; version 9 had a lot of changes to the front end and the low level box stuff, so I think it's just a matter of not updating them because it's low priority. Maybe it might be fixed (or not) in 9.0.4 –  rm -rf Apr 3 '13 at 14:13
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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