예제 #1
0
파일: base.py 프로젝트: vdonnefort/lisa
    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')
예제 #2
0
    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,
            )
예제 #3
0
    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')
예제 #4
0
            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
예제 #5
0
파일: base.py 프로젝트: vdonnefort/lisa
            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
예제 #6
0
        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