from __future__ import absolute_import, division, unicode_literals
import os
import base64
from io import BytesIO
from tempfile import NamedTemporaryFile
from contextlib import contextmanager
from itertools import chain
import param
import matplotlib as mpl
from matplotlib import pyplot as plt
from param.parameterized import bothmethod
from ...core import HoloMap
from ...core.options import Store
from ..renderer import Renderer, MIME_TYPES, HTML_TAGS
from .util import get_tight_bbox, get_old_rcparams
[docs]class OutputWarning(param.Parameterized):pass
outputwarning = OutputWarning(name='Warning')
# <format name> : (animation writer, format, anim_kwargs, extra_args)
ANIMATION_OPTS = {
'webm': ('ffmpeg', 'webm', {},
['-vcodec', 'libvpx-vp9', '-b', '1000k']),
'mp4': ('ffmpeg', 'mp4', {'codec': 'libx264'},
['-pix_fmt', 'yuv420p']),
'gif': ('pillow', 'gif', {'fps': 10}, []),
'scrubber': ('html', None, {'fps': 5}, None)
}
[docs]class MPLRenderer(Renderer):
"""
Exporter used to render data from matplotlib, either to a stream
or directly to file.
The __call__ method renders an HoloViews component to raw data of
a specified matplotlib format. The save method is the
corresponding method for saving a HoloViews objects to disk.
The save_fig and save_anim methods are used to save matplotlib
figure and animation objects. These match the two primary return
types of plotting class implemented with matplotlib.
"""
drawn = {}
backend = param.String('matplotlib', doc="The backend name.")
dpi=param.Integer(72, doc="""
The render resolution in dpi (dots per inch)""")
fig = param.ObjectSelector(default='auto',
objects=['png', 'svg', 'pdf', 'pgf',
'html', None, 'auto'], doc="""
Output render format for static figures. If None, no figure
rendering will occur. """)
holomap = param.ObjectSelector(default='auto',
objects=['widgets', 'scrubber', 'webm','mp4', 'gif', None, 'auto'], doc="""
Output render multi-frame (typically animated) format. If
None, no multi-frame rendering will occur.""")
interactive = param.Boolean(default=False, doc="""
Whether to enable interactive plotting allowing interactive
plotting with explicitly calling show.""")
mode = param.ObjectSelector(default='default', objects=['default'])
mode_formats = {'fig': ['png', 'svg', 'pdf', 'pgf', 'html', None, 'auto'],
'holomap': ['widgets', 'scrubber', 'webm','mp4', 'gif',
'html', None, 'auto']}
counter = 0
[docs] def show(self, obj):
"""
Renders the supplied object and displays it using the active
GUI backend.
"""
if self.interactive:
if isinstance(obj, list):
return [self.get_plot(o) for o in obj]
return self.get_plot(obj)
from .plot import MPLPlot
MPLPlot._close_figures = False
try:
plots = []
objects = obj if isinstance(obj, list) else [obj]
for o in objects:
plots.append(self.get_plot(o))
plt.show()
except:
raise
finally:
MPLPlot._close_figures = True
return plots[0] if len(plots) == 1 else plots
[docs] @classmethod
def plot_options(cls, obj, percent_size):
"""
Given a holoviews object and a percentage size, apply heuristics
to compute a suitable figure size. For instance, scaling layouts
and grids linearly can result in unwieldy figure sizes when there
are a large number of elements. As ad hoc heuristics are used,
this functionality is kept separate from the plotting classes
themselves.
Used by the IPython Notebook display hooks and the save
utility. Note that this can be overridden explicitly per object
using the fig_size and size plot options.
"""
from .plot import MPLPlot
factor = percent_size / 100.0
obj = obj.last if isinstance(obj, HoloMap) else obj
options = Store.lookup_options(cls.backend, obj, 'plot').options
fig_size = options.get('fig_size', MPLPlot.fig_size)*factor
return dict({'fig_size':fig_size},
**MPLPlot.lookup_options(obj, 'plot').options)
@bothmethod
def get_size(self_or_cls, plot):
w, h = plot.state.get_size_inches()
dpi = self_or_cls.dpi if self_or_cls.dpi else plot.state.dpi
return (int(w*dpi), int(h*dpi))
def _figure_data(self, plot, fmt, bbox_inches='tight', as_script=False, **kwargs):
"""
Render matplotlib figure object and return the corresponding
data. If as_script is True, the content will be split in an
HTML and a JS component.
Similar to IPython.core.pylabtools.print_figure but without
any IPython dependency.
"""
if fmt in ['gif', 'mp4', 'webm']:
with mpl.rc_context(rc=plot.fig_rcparams):
anim = plot.anim(fps=self.fps)
data = self._anim_data(anim, fmt)
else:
fig = plot.state
traverse_fn = lambda x: x.handles.get('bbox_extra_artists', None)
extra_artists = list(
chain.from_iterable(artists for artists in plot.traverse(traverse_fn)
if artists is not None)
)
kw = dict(
format=fmt,
facecolor=fig.get_facecolor(),
edgecolor=fig.get_edgecolor(),
dpi=self.dpi,
bbox_inches=bbox_inches,
bbox_extra_artists=extra_artists
)
kw.update(kwargs)
# Attempts to precompute the tight bounding box
try:
kw = self._compute_bbox(fig, kw)
except:
pass
bytes_io = BytesIO()
fig.canvas.print_figure(bytes_io, **kw)
data = bytes_io.getvalue()
if as_script:
b64 = base64.b64encode(data).decode("utf-8")
(mime_type, tag) = MIME_TYPES[fmt], HTML_TAGS[fmt]
src = HTML_TAGS['base64'].format(mime_type=mime_type, b64=b64)
html = tag.format(src=src, mime_type=mime_type, css='')
return html
if fmt == 'svg':
data = data.decode('utf-8')
return data
def _anim_data(self, anim, fmt):
"""
Render a matplotlib animation object and return the corresponding data.
"""
(writer, _, anim_kwargs, extra_args) = ANIMATION_OPTS[fmt]
if extra_args != []:
anim_kwargs = dict(anim_kwargs, extra_args=extra_args)
if self.fps is not None: anim_kwargs['fps'] = max([int(self.fps), 1])
if self.dpi is not None: anim_kwargs['dpi'] = self.dpi
if not hasattr(anim, '_encoded_video'):
# Windows will throw PermissionError with auto-delete
with NamedTemporaryFile(suffix='.%s' % fmt, delete=False) as f:
anim.save(f.name, writer=writer, **anim_kwargs)
video = f.read()
f.close()
os.remove(f.name)
return video
def _compute_bbox(self, fig, kw):
"""
Compute the tight bounding box for each figure once, reducing
number of required canvas draw calls from N*2 to N+1 as a
function of the number of frames.
Tight bounding box computing code here mirrors:
matplotlib.backend_bases.FigureCanvasBase.print_figure
as it hasn't been factored out as a function.
"""
fig_id = id(fig)
if kw['bbox_inches'] == 'tight':
if not fig_id in MPLRenderer.drawn:
fig.set_dpi(self.dpi)
fig.canvas.draw()
extra_artists = kw.pop("bbox_extra_artists", [])
pad = mpl.rcParams['savefig.pad_inches']
bbox_inches = get_tight_bbox(fig, extra_artists, pad=pad)
MPLRenderer.drawn[fig_id] = bbox_inches
kw['bbox_inches'] = bbox_inches
else:
kw['bbox_inches'] = MPLRenderer.drawn[fig_id]
return kw
[docs] @classmethod
@contextmanager
def state(cls):
old_rcparams = get_old_rcparams()
try:
cls._rcParams = old_rcparams
yield
finally:
mpl.rcParams.clear()
mpl.rcParams.update(cls._rcParams)
[docs] @classmethod
def load_nb(cls, inline=True):
"""
Initialize matplotlib backend
"""
import matplotlib.pyplot as plt
backend = plt.get_backend()
if backend not in ['agg', 'module://ipykernel.pylab.backend_inline']:
plt.switch_backend('agg')