Suppose I have several plots that represent different views of a common set of time sequence data but each Plot
contains too much data (all of which I want available for inspection) to fit nicely within the width of a notebook on my screen. My idea is to scale up each plot to a size that makes it easy to read, put each plot in its own horizontally scrollable Pane
, stack them up vertically in the notebook, and use a Dynamic
setting for the ScrollPosition
of each Pane
so they are all synched up to display the same data element in the center of each Pane
.
The code I developed does just this, but it's running into problems when it is invoked more than once in the same notebook. The problem apparently stems from the non-local nature of DynamicModule
variables. Whereas it is possible to call a Function
containing a Module
multiple times without the local variables from different calls ever interacting with each other, this does not seem to be the case where DynamicModule
variables are concerned.
I have read other MSE posts on the similar topics (e.g., Internal DynamicModule steals scope of external DynamicModule) but haven't been able to figure out how to apply the suggestions presented there to my particular case.
My code is shown below in a much simplified form. The essential elements are contained in two functions named PanedPlot
and SynchedPlots
. ScrollablePaneWidth
is just a method for finding a Pane
width that fits nicely inside a notebook.
Clear[PanedPlot]
SetAttributes[PanedPlot, HoldFirst]
PanedPlot[{r_, firstPassQ_, xMax_}, {plot_, paneWidth_}] :=
DynamicModule[{x},
Labeled[
Pane[
plot, paneWidth, Scrollbars -> {Automatic, False},
ScrollPosition -> Dynamic[
If[firstPassQ, {xMax, 0}, {x = xMax r, 0}],
If[firstPassQ, xMax = First@#, x = First@#; r = x/xMax] &
]
], {firstPassQ, r, x, xMax} // Dynamic, Top
]
]
A few comments about PanedPlot
:
The HoldFirst
attribute for PanedPlot
causes it to emulate 'pass by reference' functionality for function parameters r
, firstPassQ
, and xMax
.
firstPassQ
is a control flag. On the first pass, xMax
gets set to the maximum value of the scroll position (assuming xMax
started out with a greater value than this). On subsequent passes, the code sets the scroll position from the saved value of xMax
and the relative scroll position r
, which is in turn determined by the position of the present scroll bar or the other scroll bar, whichever one was actively moved. That's how pane position synchronization is maintained. (In my actual code, it's a more complicated relationship, but it follows the same principle.)
Clear[SynchedPlots]
SynchedPlots[plotA_, plotB_, paneWidth_] :=
DynamicModule[{firstPassQ = True, r = 0},
DynamicModule[{xMaxA = 10^10, xMaxB = 10^10},
Column[{
PanedPlot[{r, firstPassQ, xMaxA}, {plotA, paneWidth}],
PanedPlot[{r, firstPassQ, xMaxB}, {plotB, paneWidth}]
}] // Dynamic
] // Print;
FinishDynamic[];
firstPassQ = False;
]
A few comments about SynchedPlots
:
On first invocation, firstPassQ
is set to True
in the outer DynamicModule
to tell PanedPlot
to determine and save the value of xMax
appropriate to its plot. However, this won't actually happen until each plot has finished displaying, which is the reason for FinishDynamic
and the nested DynamicModule
s. The call to FinishDynamic
guarantees that xMaxA
and xMaxB
get updated before firstPassQ
is set to False
.
Clear[ScrollablePaneWidth]
ScrollablePaneWidth[] :=
With[{windowWidth =
First[WindowSize /.
AbsoluteOptions[EvaluationNotebook[], WindowSize]],
magnification =
Magnification /.
AbsoluteOptions[EvaluationNotebook[], Magnification],
cellMargin =
Total@First[
CellMargins /. AbsoluteOptions[EvaluationCell[], CellMargins]]},
(windowWidth - 34)/magnification - cellMargin - 20
]
Any single call to SynchedPlots
works as designed. For example
aspectRatio = .1;
imageSize = 2000;
paneWidth = ScrollablePaneWidth[];
plot =
Map[Plot[#[[1]][x], {x, 0, 2 \[Pi]}, Frame -> True,
Background -> #[[3]], AspectRatio -> aspectRatio/#[[2]],
ImageSize -> #[[2]] imageSize] &,
Transpose@{{Sin, Cos, Csc, Tan}, {1, 1.5, 1.3, 1.8}, {LightYellow,
LightOrange, LightGreen, LightBlue}}];
followed by
SynchedPlots[plot[[1]], plot[[2]], paneWidth];
works fine. Here is a snapshot of what the display looks like very close to the middle of the scroll bar range
However, the next time it is called in the same notebook, such as with
SynchedPlots[plot[[3]], plot[[4]], paneWidth];
the newest SynchedPlots
display works until you go back and try to move the scroll bars in the previous one, at which point all the scroll bars freeze up. Re-evaluate either cell and its display will work again as long as you don't touch the other one.
The idea that the problem stems from non-independent dynamic variables is reinforced by what happens when both SynchedPlots
are evaluated inside a single Module
, as shown below.
Module[{aspectRatio = .1, imageSize = 2000,
paneWidth = ScrollablePaneWidth[], plot},
plot =
Map[Plot[#[[1]][x], {x, 0, 2 \[Pi]}, Frame -> True,
Background -> #[[3]], AspectRatio -> aspectRatio/#[[2]],
ImageSize -> #[[2]] imageSize] &,
Transpose@{{Sin, Cos, Csc, Tan}, {1, 1.5, 1.3, 1.8}, {LightYellow,
LightOrange, LightGreen, LightBlue}}];
{SynchedPlots[plot[[1]], plot[[2]], paneWidth],
SynchedPlots[plot[[3]], plot[[4]], paneWidth]}
]
With only one SynchedPlots
call, everything is fine. But with two the result looks like
Right from the start none of the scroll bars work. The plot labels suggest that the dynamic parameters firstPassQ
, r
, and x
are not being evaluated properly (whereas the values of xMax
are).
What's going wrong and how exactly does one fix it?
It's nice to have this type of functionality available in Mathematica for displaying large data sets. It would be even better if it could be used more than once in a single notebook.
SynchedPlots
would have a different value ofpaneWidth
upon evaluation. When you evaluate one, it is fine andpaneWidth
is set, then you evaluate the next one & redefinepaneWidth
, so it is fine now only for the second one you have evaluated. If you try to evaluate two, which shouldpaneWidth
be valid for? I would say none of them. You should find a different way to use or definepaneWidth
. $\endgroup$paneWidth
is computed once and passed by value. The width setting is the same for eachPane
by design. $\endgroup$