Esempio n. 1
0
def make_figure(width, height, nrows, ncols, interactive=None, **kwargs):
    """
    Make a :class:`matplotlib.figure.Figure` and its axes.

    :param width: Width of the figure.
    :type width: int

    :param height: Height of the figure.
    :type height: int

    :param interactive: If ``True``, create an interactive figure. Defaults to
        ``True`` when running under IPython, ``False`` otherwise.
    :type interactive: bool or None

    :Variable keyword arguments: Forwarded to :class:`matplotlib.figure.Figure`

    :returns: A tuple of:
        * :class:`matplotlib.figure.Figure`
        * :class:`matplotlib.axes.Axes` as a scalar, an iterable (1D) or iterable of iterable matrix (2D)
    """
    if interactive is None:
        interactive = is_running_ipython()

    if not interactive and tuple(map(int,
                                     mpl.__version__.split('.'))) <= (3, 0, 3):
        warnings.warn(
            'This version of matplotlib does not allow saving figures from axis created using Figure(), forcing interactive=True'
        )
        interactive = True

    width *= ncols
    height *= nrows

    if interactive:
        figure, axes = plt.subplots(
            figsize=(width, height),
            nrows=nrows,
            ncols=ncols,
            **kwargs,
        )
    else:
        figure = Figure(figsize=(width, height))
        axes = figure.subplots(ncols=ncols, nrows=nrows, **kwargs)

    return (figure, axes)
Esempio n. 2
0
    def setup_plot(cls,
                   width=16,
                   height=4,
                   ncols=1,
                   nrows=1,
                   interactive=None,
                   link_dataframes=None,
                   cursor_delta=None,
                   **kwargs):
        """
        Common helper for setting up a matplotlib plot

        :param width: Width of the plot (inches)
        :type width: int or float

        :param height: Height of each subplot (inches)
        :type height: int or float

        :param ncols: Number of plots on a single row
        :type ncols: int

        :param nrows: Number of plots in a single column
        :type nrows: int

        :param link_dataframes: Link the provided dataframes to the axes using
            :func:`lisa.notebook.axis_link_dataframes`
        :type link_dataframes: list(pandas.DataFrame) or None

        :param cursor_delta: Add two vertical lines set with left and right
            clicks, and show the time delta between them in a widget.
        :type cursor_delta: bool or None

        :param interactive: If ``True``, use the pyplot API of matplotlib,
            which integrates well with notebooks. However, it can lead to
            memory leaks in scripts generating lots of plots, in which case it
            is better to use the non-interactive API. Defaults to ``True`` when
            running under IPython or Jupyter notebook, `False`` otherwise.
        :type interactive: bool

        :Keywords arguments: Extra arguments to pass to
          :obj:`matplotlib.figure.Figure.subplots`

        :returns: tuple(matplotlib.figure.Figure, matplotlib.axes.Axes (or an
          array of, if ``nrows`` > 1))
        """

        figure, axes = make_figure(
            interactive=interactive,
            width=width,
            height=height,
            ncols=ncols,
            nrows=nrows,
            **kwargs,
        )
        if interactive is None:
            interactive = is_running_ipython()

        use_widgets = interactive

        if link_dataframes:
            if not use_widgets:
                cls.get_logger().error(
                    'Dataframes can only be linked to axes in interactive widget plots'
                )
            else:
                for axis in figure.axes:
                    axis_link_dataframes(axis, link_dataframes)

        if cursor_delta or cursor_delta is None and use_widgets:
            if not use_widgets and cursor_delta is not None:
                cls.get_logger().error(
                    'Cursor delta can only be used in interactive widget plots'
                )
            else:
                for axis in figure.axes:
                    axis_cursor_delta(axis)

        for axis in figure.axes:
            axis.relim(visible_only=True)
            axis.autoscale_view(True)

        # Needed for multirow plots to not overlap with each other
        figure.set_tight_layout(dict(h_pad=3.5))
        return figure, axes
Esempio n. 3
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
Esempio n. 4
0
    def setup_plot(cls, width=16, height=4, ncols=1, nrows=1, interactive=None, link_dataframes=None, cursor_delta=None, **kwargs):
        """
        Common helper for setting up a matplotlib plot

        :param width: Width of the plot (inches)
        :type width: int or float

        :param height: Height of each subplot (inches)
        :type height: int or float

        :param ncols: Number of plots on a single row
        :type ncols: int

        :param nrows: Number of plots in a single column
        :type nrows: int

        :param link_dataframes: Link the provided dataframes to the axes using
            :func:`lisa.notebook.axis_link_dataframes`
        :type link_dataframes: list(pandas.DataFrame) or None

        :param cursor_delta: Add two vertical lines set with left and right
            clicks, and show the time delta between them in a widget.
        :type cursor_delta: bool or None

        :param interactive: If ``True``, use the pyplot API of matplotlib,
            which integrates well with notebooks. However, it can lead to
            memory leaks in scripts generating lots of plots, in which case it
            is better to use the non-interactive API. Defaults to ``True`` when
            running under IPython or Jupyter notebook, `False`` otherwise.
        :type interactive: bool

        :Keywords arguments: Extra arguments to pass to
          :obj:`matplotlib.figure.Figure.subplots`

        :returns: tuple(matplotlib.figure.Figure, matplotlib.axes.Axes (or an
          array of, if ``nrows`` > 1))
        """

        running_ipython = is_running_ipython()
        if interactive is None:
            interactive = running_ipython

        if tuple(map(int, matplotlib.__version__.split('.'))) <= (3, 0, 3):
            warnings.warn('This version of matplotlib does not allow saving figures from axis created using Figure(), forcing interactive=True')
            interactive = True

        if interactive:
            figure, axes = plt.subplots(
                ncols=ncols, nrows=nrows, figsize=(width, height * nrows),
                **kwargs
            )
        else:
            figure = Figure(figsize=(width, height * nrows))
            axes = figure.subplots(ncols=ncols, nrows=nrows, **kwargs)

        if isinstance(axes, Iterable):
            ax_list = axes
        else:
            ax_list = [axes]

        use_widgets = interactive and running_ipython

        if link_dataframes:
            if not use_widgets:
                cls.get_logger().error('Dataframes can only be linked to axes in interactive widget plots')
            else:
                for axis in ax_list:
                    axis_link_dataframes(axis, link_dataframes)

        if cursor_delta or cursor_delta is None and use_widgets:
            if not use_widgets and cursor_delta is not None:
                cls.get_logger().error('Cursor delta can only be used in interactive widget plots')
            else:
                for axis in ax_list:
                    axis_cursor_delta(axis)

        # Needed for multirow plots to not overlap with each other
        figure.set_tight_layout(dict(h_pad=3.5))
        return figure, axes
Esempio n. 5
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
Esempio n. 6
0
            def wrapper(self,
                        *args,
                        filepath=None,
                        axis=None,
                        output=None,
                        img_format=None,
                        always_save=False,
                        colors=None,
                        **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__,
                    )

                if colors:
                    cycler = make_cycler(color=colors)
                    set_cycler = lambda axis: cls.set_axis_cycler(axis, cycler)
                else:
                    set_cycler = 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):
                        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)

                    with set_cycler(axis):
                        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

                    # 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, 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