try:
import holoviews.plotting.mpl
except (ImportError, RuntimeError):
hv = False
from matplotlib import pyplot as plt
from matplotlib.figure import Figure
from matplotlib.gridspec import GridSpec, GridSpecFromSubplotSpec
# hv.extension('matplotlib') # Only use this in notebooks.
if hv:
class FigurePlot(hv.plotting.mpl.ElementPlot):
def initialize_plot(self, ranges=None):
element = self.hmap.last
return element.data
def update_frame(self, key, ranges=None):
element = self._get_frame(key)
self.handles["fig"] = element.data
return element.data
hv.Store.register({MPLFigure: FigurePlot}, "matplotlib")
"""
from matplotlib import pyplot as plt
def figure_fn(x):
fig, ax = plt.subplots()
ax.plot([0,1,x])
plt.close(fig)
return MPLFigure(fig)
hv.HoloMap({i: figure_fn(i) for i in range(10)})
"""
[docs]
class MPLGrid:
"""Object for stacking plots.
After initializing the object, call next() to get the next axis. By
default axes will stack vertically.
Attributes
----------
down : bool
If True, then stack elements vertically growing down. Otherwise stack
elements horizontally growing right.
ratios : [float]
List of relative size of each entry.
axes : [Axes, MPLGrid]
List of axes of nested grid objects.
direction : ['down', 'right']
Direction of growth.
right : bool
Specify if y axis labels should be on the right or left.
scale : bool
If False, then when a parent MPLGrid is being adjusted, the ratio for
this grid component will be the sum of the ratios rather than the
specified parent ratio. If True, then all the components will be
scaled to fit into the parents ratio.
share : bool
If `True`, then axes in the growth direction are shared, and tick labels
etc. are suppressed except for the final one.
space : float, None
Space (hspace or wspace) between grids. If this is None, then the space
is set of zero if the axes are shared and 0.1 otherwise.
size : float
Default size for new elements.
"""
def __init__(
self,
fig=None,
subplot_spec=None,
ax=None,
direction="down",
right=False,
scale=True,
space=None,
share=False,
size=1,
**kw,
):
if fig is None:
fig = plt.gcf()
else:
if ax is not None:
fig.delaxes(ax)
if subplot_spec is None:
if ax is not None:
try:
subplot_spec = ax.get_subplotspec()
except AttributeError:
pass
if subplot_spec is None:
subplot_spec = GridSpec(1, 1)[0, 0]
[docs]
self.subplot_spec = subplot_spec
if direction not in set(["down", "right"]):
raise ValueError(
"direction must be 'down' or 'right': got direction={}".format(direction)
)
[docs]
self.direction = direction
[docs]
def __getitem__(self, key):
return self.axes[key]
[docs]
def adjust(self):
"""Adjust the positions of all the axes."""
ax_ = None # Previous axis
axs = list(zip(self.axes, self._gridspec))
if self.direction == "right" and not self.right:
# Make sure the last axis is the one that should have labels.
axs = reversed(axs)
for ax, subplot_spec in axs:
if isinstance(ax, MPLGrid):
ax.subplot_spec = subplot_spec
ax.adjust()
ax_ = None
continue
if not ax:
break
ax.set_position(subplot_spec.get_position(self.fig))
ax.set_subplotspec(subplot_spec) # necessary if using tight_layout()
if self.direction == "down":
if ax_ and ax.get_shared_x_axes().joined(ax, ax_):
plt.setp(ax_.get_xticklabels(), visible=False)
ax.tick_params(axis="x", direction="inout")
else:
if ax_ and ax.get_shared_y_axes().joined(ax, ax_):
plt.setp(ax_.get_yticklabels(), visible=False)
ax.tick_params(axis="y", direction="inout")
if self.right:
ax.yaxis.tick_right()
ax.yaxis.set_label_position("right")
ax_ = ax
[docs]
def __repr__(self):
res = []
for ratio, ax in zip(self.ratios, self.axes):
if isinstance(ax, MPLGrid):
res.append(ax)
else:
res.append(ratio)
return repr(res)
@property
[docs]
def ratios(self):
"""Return the size ratios for the various axes.
If the sub-axis is an MPLGrid instance with scale == False, then the
ratio is the sum of the sub-grid's ratios.
"""
ratios = []
for ratio, axis in zip(self._ratios, self.axes):
if isinstance(axis, MPLGrid) and not axis.scale:
ratio = sum(axis.ratios)
ratios.append(ratio)
return ratios
@property
[docs]
def _gridspec(self):
ratios = self.ratios
N = len(ratios)
if N == 0:
# This can happen during initialization
N = 1
ratios = [1]
space = self.space
if space is None:
space = 0 if self.share else 0.1
args = dict(
nrows=1, ncols=1, wspace=space, hspace=space, subplot_spec=self.subplot_spec
)
if self.direction == "down":
args["nrows"] = N
args["height_ratios"] = ratios
else:
args["ncols"] = N
args["width_ratios"] = ratios
args.update(self.kw)
if hasattr(self, "debug"):
print(id(self), self.subplot_spec)
return GridSpecFromSubplotSpec(**args)
[docs]
def next(self, size=None):
"""Return the next axis."""
if size is None:
size = self.size
if size == 0:
return None
self._ratios.append(size)
ax_prev = self.axes[-1] if self.axes else None
self.axes.append(None)
args = {}
if self.share and ax_prev:
if self.direction == "down":
args["sharex"] = ax_prev
else:
args["sharey"] = ax_prev
ax = self.fig.add_subplot(self._gridspec[-1], **args)
self.axes[-1] = ax
self.adjust()
return ax
[docs]
def empty(self, size=None):
"""Return the next axis but hide it so it acts as a space."""
ax = self.next(size=size)
ax.set_visible(False)
return ax
[docs]
def grid(self, size=None, **kw):
"""Insert a sub-grid and return it."""
if size is None:
size = self.size
if size == 0:
return None
self._ratios.append(size)
self.axes.append(None)
grid = MPLGrid(fig=self.fig, subplot_spec=self._gridspec[-1], **kw)
self.axes[-1] = grid
self.adjust()
return grid
[docs]
def rescale(self, tight=None, scalex=True, scaley=True, force=False):
for ax in self.axes:
if isinstance(ax, MPLGrid):
ax.rescale(tight=tight, scalex=scalex, scaley=scaley)
else:
if force:
ax.set_autoscale_on(True)
ax.relim()
ax.autoscale_view(tight=tight, scalex=scalex, scaley=scaley)