def save_plot(self, figure, filepath=None, img_format=None): """ Save a :class:`matplotlib.figure.Figure` as an image file. """ img_format = img_format or guess_format(filepath) or 'png' caller = inspect.stack()[1][3] filepath = filepath or self.get_default_plot_path( img_format=img_format, plot_name=caller, ) figure.savefig(filepath, format=img_format, bbox_inches='tight')
def save_plot(self, figure, filepath=None, img_format=None, backend=None): """ Save a holoviews element or :class:`matplotlib.figure.Figure` as an image file. :param figure: Figure to save to a file. :type figure: matplotlib.figure.Figure or holoviews.core.Element :param filepath: Path to the file to save the plot. If ``None``, a default path will be used. :type filepath: str or None :param img_format: Format of the image. If ``None``, it is guessed from the ``filepath``. :type img_format: str or None :param backend: Holoviews backend to use. If left to ``None``, the current backend enabled with ``hv.extension()`` will be used. :type backend: str or None """ img_format = img_format or guess_format(filepath) or 'png' filepath = filepath or self.get_default_plot_path( img_format=img_format, # Use the caller's name as plot name plot_name=inspect.stack()[1].function, ) if isinstance(figure, matplotlib.figure.Figure): # The suptitle is not taken into account by tight layout by default: # https://stackoverflow.com/questions/48917631/matplotlib-how-to-return-figure-suptitle suptitle = figure._suptitle figure.savefig( filepath, bbox_extra_artists=[suptitle] if suptitle else None, format=img_format, bbox_inches='tight' ) else: self._fig_as_plot_method( figure, filepath=filepath, backend=backend, )
def save_plot(self, figure, filepath=None, img_format=None): """ Save a :class:`matplotlib.figure.Figure` as an image file. """ img_format = img_format or guess_format(filepath) or 'png' caller = inspect.stack()[1][3] filepath = filepath or self.get_default_plot_path( img_format=img_format, plot_name=caller, ) # The suptitle is not taken into account by tight layout by default: # https://stackoverflow.com/questions/48917631/matplotlib-how-to-return-figure-suptitle suptitle = figure._suptitle figure.savefig(filepath, bbox_extra_artists=[suptitle] if suptitle else None, format=img_format, bbox_inches='tight')
def wrapper(self, *args, filepath=None, axis=None, output=None, img_format=None, always_save=False, colors: TypedList[str] = None, linestyles: TypedList[str] = None, markers: TypedList[str] = None, rc_params=None, **kwargs): def is_f_param(param): """ Return True if the parameter is for `f`, False if it is for setup_plot() """ try: desc = inspect.signature(f).parameters[param] except KeyError: return False else: # Passing kwargs=42 to a function taking **kwargs # should not return True here, as we only consider # explicitly listed arguments return desc.kind not in ( inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL, ) # Factor the *args inside the **kwargs by binding them to the # user-facing signature, which is the one of the wrapper. kwargs.update( inspect.signature(wrapper).bind_partial(self, *args).arguments) f_kwargs = { param: val for param, val in kwargs.items() if is_f_param(param) } img_format = img_format or guess_format(filepath) or 'png' local_fig = axis is None # When we create the figure ourselves, always save the plot to # the default location if local_fig and filepath is None and always_save: filepath = self.get_default_plot_path( img_format=img_format, plot_name=f.__name__, ) cyclers = dict( color=colors, linestyle=linestyles, marker=markers, ) cyclers = { name: value for name, value in cyclers.items() if value } if cyclers: cyclers = [ make_cycler(**{name: value}) for name, value in cyclers.items() ] set_cycler = lambda axis: cls.set_axis_cycler( axis, *cyclers) else: set_cycler = lambda axis: nullcontext() if rc_params: set_rc_params = lambda axis: cls.set_axis_rc_params( axis, rc_params) else: set_rc_params = lambda axis: nullcontext() # Allow returning an axis directly, or just update a given axis if return_axis: # In that case, the function takes all the kwargs with set_cycler(axis), set_rc_params(axis): axis = f(**kwargs, axis=axis) else: if local_fig: setup_plot_kwargs = { param: val for param, val in kwargs.items() if param not in f_kwargs } fig, axis = self.setup_plot(**setup_plot_kwargs) f_kwargs.update( axis=axis, local_fig=f_kwargs.get('local_fig', local_fig), ) with set_cycler(axis), set_rc_params(axis): f(**f_kwargs) if isinstance(axis, numpy.ndarray): fig = axis[0].get_figure() else: fig = axis.get_figure() def resolve_formatter(fmt): format_map = { 'rst': cls._get_rst_content, 'html': cls._get_html, } try: return format_map[fmt] except KeyError: raise ValueError(f'Unsupported format: {fmt}') if output is None: out = axis # Show the LISA figure toolbar if is_running_ipython(): # Make sure we only add one button per figure try: toolbar = self._get_fig_data(fig, 'toolbar') except KeyError: toolbar = self._make_fig_toolbar(fig) self._set_fig_data(fig, 'toolbar', toolbar) display(toolbar) mplcursors.cursor(fig) else: out = resolve_formatter(output)(f, [], f_kwargs, axis) if filepath: if img_format in ('html', 'rst'): content = resolve_formatter(img_format)(f, [], f_kwargs, axis) with open(filepath, 'wt', encoding='utf-8') as fd: fd.write(content) else: fig.savefig(filepath, format=img_format, bbox_inches='tight') return out
def wrapper(self, *args, filepath=None, axis=None, output=None, img_format=None, always_save=True, **kwargs): # Bind the function to the instance, so we avoid having "self" # showing up in the signature, which breaks parameter # formatting code. f = func.__get__(self, type(self)) def is_f_param(param): """ Return True if the parameter is for `f`, False if it is for setup_plot() """ try: desc = inspect.signature(f).parameters[param] except KeyError: return False else: # Passing kwargs=42 to a function taking **kwargs # should not return True here, as we only consider # explicitely listed arguments return desc.kind not in ( inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL, ) f_kwargs = { param: val for param, val in kwargs.items() if is_f_param(param) } img_format = img_format or guess_format(filepath) or 'png' local_fig = axis is None # When we create the figure ourselves, always save the plot to # the default location if local_fig and filepath is None and always_save: filepath = self.get_default_plot_path( img_format=img_format, plot_name=f.__name__, ) # Allow returning an axis directly, or just update a given axis if return_axis: # In that case, the function takes all the kwargs axis = f(*args, **kwargs, axis=axis) else: if local_fig: setup_plot_kwargs = { param: val for param, val in kwargs.items() if param not in f_kwargs } fig, axis = self.setup_plot(**setup_plot_kwargs) f(*args, axis=axis, local_fig=local_fig, **f_kwargs) if isinstance(axis, numpy.ndarray): fig = axis[0].get_figure() else: fig = axis.get_figure() def resolve_formatter(fmt): format_map = { 'rst': cls._get_rst_content, 'html': cls._get_html, } try: return format_map[fmt] except KeyError: raise ValueError('Unsupported format: {}'.format(fmt)) if output is None: out = axis else: out = resolve_formatter(output)(f, args, f_kwargs, axis) if filepath: if img_format in ('html', 'rst'): content = resolve_formatter(img_format)(f, args, f_kwargs, axis) with open(filepath, 'wt', encoding='utf-8') as fd: fd.write(content) else: fig.savefig(filepath, format=img_format, bbox_inches='tight') return out
def wrapper(self, *args, filepath=None, output='holoviews', img_format=None, always_save=False, backend=None, _compat_render=False, link_dataframes=None, cursor_delta=None, width=None, height=None, # Deprecated parameters rc_params=None, axis=None, interactive=None, colors: TypedList[str]=None, linestyles: TypedList[str]=None, markers: TypedList[str]=None, **kwargs ): def deprecation_warning(msg): warnings.warn( msg, DeprecationWarning, stacklevel=2, ) if interactive is not None: deprecation_warning( '"interactive" parameter is deprecated and ignored', ) interactive = is_running_ipython() # If the user did not specify a backend, we will return a # holoviews object, but we need to know what is the current # backend so we can apply the relevant options. if backend is None: backend = hv.Store.current_backend # For backward compat, return a matplotlib Figure when this # backend is selected if output is None and _compat_render and backend == 'matplotlib': output = 'render' # Before this point "None" indicates the default. if output is None: # TODO: Switch the default to be "ui" when interactive once a # solution is found for that issue: # https://discourse.holoviz.org/t/replace-holoviews-notebook-rendering-with-a-panel/2519/12 # output = 'ui' if interactive else 'holoviews' output = 'holoviews' # Deprecated, but allows easy backward compat if axis is not None: output = 'render' deprecation_warning( 'axis parameter is deprecated, use holoviews APIs to combine plots (see overloading of ``*`` operator for holoviews elements)' ) if link_dataframes and output != 'ui': warnings.warn(f'"link_dataframes" parameter ignored since output != "ui"', stacklevel=2) img_format = img_format or guess_format(filepath) or 'png' # When we create the figure ourselves, always save the plot to # the default location if filepath is None and always_save: filepath = self.get_default_plot_path( img_format=img_format, plot_name=f.__name__, ) # Factor the *args inside the **kwargs by binding them to the # user-facing signature, which is the one of the wrapper. kwargs.update( inspect.signature(wrapper).bind_partial(self, *args).arguments ) with lisa.notebook._hv_set_backend(backend): hv_fig = f(**kwargs) # For each element type, only set the option if it has not # been set already. This allows the plot method to give # customized options that will not be overridden here. set_by_method = {} for category in ('plot', 'style'): for name, _opts in hv_fig.traverse( lambda element: ( element.__class__.name, hv.Store.lookup_options( backend, element, category ).kwargs.keys() ) ): set_by_method.setdefault(name, set()).update(_opts) def set_options(fig, opts, typs): return fig.options( { typ: { k: v for k, v in opts.items() if k not in set_by_method.get(typ, tuple()) } for typ in typs }, # Specify the backend explicitly, in case the user # asked for a specific backend backend=backend, ) def set_option(fig, name, val, typs, extra=None): return set_options( fig=fig, opts={name: val, **(extra or {})}, typs=typs, ) def set_cycle(fig, name, xs, typs, extra=None): return set_option( fig=fig, name=name, val=hv.Cycle(xs), typs=typs, extra=extra, ) # Deprecated options if colors: deprecation_warning( '"colors" is deprecated and has no effect anymore, use .options() on the resulting holoviews object' ) if markers: deprecation_warning( '"markers" is deprecated and has no effect anymore, use .options() on the resulting holoviews object' ) if linestyles: deprecation_warning( '"linestyles" is deprecated and has no effect anymore, use .options() on the resulting holoviews object' ) if rc_params: deprecation_warning( 'rc_params deprecated, use holoviews APIs to set matplotlib parameters' ) if backend == 'matplotlib': hv_fig = hv_fig.opts(fig_rcparams=rc_params) else: self.logger.warning('rc_params is only used with matplotlib backend') # Markers added by lisa.notebook.plot_signal if backend == 'bokeh': marker_opts = dict( # Disable muted legend for now, as they will mute # everything: # https://github.com/holoviz/holoviews/issues/3936 # legend_muted=True, muted_alpha=0, tools=[], ) elif backend == 'matplotlib': # Hide the markers since it clutters static plots, making # them hard to read. marker_opts = dict( visible=False, ) else: marker_opts = {} hv_fig = set_options( hv_fig, opts=marker_opts, typs=('Scatter.marker',), ) # Tools if backend == 'bokeh': hv_fig = set_option( hv_fig, name='tools', val=[ 'undo', 'redo', 'crosshair', 'hover', ], typs=('Curve', 'Path', 'Points', 'Scatter', 'Bars', 'Histogram', 'Distribution', 'HeatMap', 'Image', 'Rectangles', 'Area', 'Spikes'), ).options( backend=backend, # Sometimes holoviews (or bokeh) decides to put it on # the side, which crops it toolbar='above', ) # Workaround: # https://github.com/holoviz/holoviews/issues/4981 hv_fig = set_option( hv_fig, name='color', val=hv.Cycle(), typs=('Rectangles',), ) # Figure size if backend in ('bokeh', 'plotly'): aspect = 4 if (width, height) == (None, None): size = dict( aspect=aspect, responsive=True, ) elif height is None: size = dict( width=width, height=int(width / aspect), ) elif width is None: size = dict( height=height, responsive=True, ) else: size = dict( width=width, height=height, ) hv_fig = set_options( hv_fig, opts=size, typs=('Curve', 'Path', 'Points', 'Scatter', 'Overlay', 'Bars', 'Histogram', 'Distribution', 'HeatMap', 'Image', 'Rectangles', 'Area', 'HLine', 'VLine', 'Spikes', 'HSpan', 'VSpan'), ) elif backend == 'matplotlib': width = 16 if width is None else width height = 4 if height is None else height fig_inches = max(width, height) hv_fig = set_options( hv_fig, opts=dict( aspect=width / height, fig_inches=fig_inches, ), typs=('Curve', 'Path', 'Points', 'Scatter', 'Overlay', 'Bars', 'Histogram', 'Distribution', 'HeatMap', 'Image', 'Rectangles', 'Area', 'HLine', 'VLine', 'Spikes'), ) # Not doing this on the Layout will prevent getting big # figures, but the "aspect" cannot be set on a Layout hv_fig = set_options( hv_fig, opts=dict(fig_inches=fig_inches), typs=('Layout',), ) # Use a memoized function to make sure we only do the rendering once @memoized def rendered_fig(): if backend == 'matplotlib': # Make sure to use an interactive renderer for notebooks, # otherwise the plot will not be displayed renderer = hv.plotting.mpl.MPLRenderer.instance( interactive=interactive ) return renderer.get_plot( hv_fig, interactive=interactive, axis=axis, fig=axis.figure if axis else None, ).state else: return hv.renderer(backend).get_plot(hv_fig).state def resolve_formatter(fmt): format_map = { 'rst': cls._get_rst_content, 'sphinx-rst': cls._get_rst_content, 'html': cls._get_html, 'sphinx-html': cls._get_html, } try: return format_map[fmt] except KeyError: raise ValueError(f'Unsupported format: {fmt}') if filepath: if backend in ('bokeh', 'matplotlib') and img_format in ('html', 'sphinx-html', 'rst', 'sphinx-rst'): content = resolve_formatter(img_format)( fmt=img_format, f=f, args=[], kwargs=kwargs, fig=rendered_fig(), backend=backend ) with open(filepath, 'wt', encoding='utf-8') as fd: fd.write(content) else: # Avoid cropping the legend on some backends static_fig = set_options( hv_fig, opts=dict(responsive=False), typs=('Curve', 'Path', 'Points', 'Scatter', 'Overlay', 'Bars', 'Histogram', 'Distribution', 'HeatMap', 'Image', 'Rectangles', 'HLine', 'VLine', 'VSpan', 'HSpan', 'Spikes'), ) hv.save(static_fig, filepath, fmt=img_format, backend=backend) if output == 'holoviews': out = hv_fig # Show the LISA figure toolbar elif output == 'ui': # TODO: improve holoviews so we can return holoviews # objects that are displayed with extra widgets around # https://discourse.holoviz.org/t/replace-holoviews-notebook-rendering-with-a-panel/2519/12 make_pane = functools.partial( self._make_fig_ui, link_dataframes=link_dataframes, ) out = _hv_fig_to_pane(hv_fig, make_pane) elif output == 'render': if _compat_render and backend == 'matplotlib': axes = rendered_fig().axes if len(axes) == 1: out = axes[0] else: out = axes else: out = rendered_fig() else: out = resolve_formatter(output)( fmt=output, f=f, args=[], kwargs=kwargs, fig=rendered_fig(), backend=backend ) return out