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:
Known issues:
- An explicit
ImageSize value must be supplied with the plot as there is no way (at least I know of) to extract this value from a plot which was generated with the default size (i.e. using Automatic, All, etc.).
- 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_] := Module[{
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
(* not fully integrated variables *)
, coord = None
},
held = Hold@defPlot;
head = Part[held, 1, 0];
plot = defPlot;
{defEpilog, defRange, defSize} = {Epilog, PlotRange, ImageSize} /. Quiet@AbsoluteOptions@plot;
defEpilog = defEpilog /. Epilog -> {};
size = defSize;
range = zoomRange = defRange;
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 = True && ! 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]];
Deploy@EventHandler[Dynamic[MouseAppearance[
Show[plot,
PlotRange -> Dynamic@range,
(*PlotLabel->Dynamic@{pt1,pt2,spt1},*)
Epilog -> {
(* Original epilog *)
defEpilog,
(* 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}]
}],
Which[
CurrentValue@"ControlKey" && !overHor && !overVer, "ZoomView", (* undocumented *)
CurrentValue@"ShiftKey" && !overHor && !overVer, "DragGraphics",
CurrentValue@"ShiftKey" && (overHor || overVer), "LinkHand",
dragged, "FrameRisingResize",
True, Automatic]
], TrackedSymbols :> {dragged, plot}],
{
(*"MouseMoved":>(coord=MousePosition@"Graphics"),*)(* This won't work *)
"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
]];
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}]

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

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

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:

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