From 45ccea7e929ce03dcd2db0120b3185f3afec817c Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 25 Sep 2024 19:54:32 +0200 Subject: [PATCH 1/5] better version of make_figure which works for both jupyter ipympl and Qt window figures from terminal --- src/plopp/__init__.py | 13 ---------- src/plopp/backends/matplotlib/utils.py | 33 ++++++++++++++++---------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/plopp/__init__.py b/src/plopp/__init__.py index 51708ec8..ec650423 100644 --- a/src/plopp/__init__.py +++ b/src/plopp/__init__.py @@ -37,18 +37,6 @@ del importlib -def show() -> None: - """ - A function to display all the currently opened figures (note that this only applies - to the figures created via the Matplotlib backend). - See https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.show.html for more - details. - """ - import matplotlib.pyplot as plt - - plt.show() - - __all__ = [ 'Camera', 'Node', @@ -65,7 +53,6 @@ def show() -> None: 'scatterfigure', 'scatter3d', 'scatter3dfigure', - 'show', 'show_graph', 'slicer', 'superplot', diff --git a/src/plopp/backends/matplotlib/utils.py b/src/plopp/backends/matplotlib/utils.py index 896e60d3..d02e47bf 100644 --- a/src/plopp/backends/matplotlib/utils.py +++ b/src/plopp/backends/matplotlib/utils.py @@ -5,10 +5,12 @@ from typing import Literal import matplotlib as mpl -from matplotlib.pyplot import Figure, _get_backend_mod +# from matplotlib.pyplot import Figure, _backend_mod, _get_backend_mod +import matplotlib.pyplot as plt -def fig_to_bytes(fig: Figure, form: Literal['png', 'svg'] = 'png') -> bytes: + +def fig_to_bytes(fig: plt.Figure, form: Literal['png', 'svg'] = 'png') -> bytes: """ Convert a Matplotlib figure to png (default) or svg bytes. @@ -31,10 +33,11 @@ def is_interactive_backend() -> bool: backend. """ backend = mpl.get_backend() - return any(x in backend for x in ("ipympl", "widget")) + # return any(x in backend for x in ("ipympl", "widget")) + return "inline" not in backend -def make_figure(*args, **kwargs) -> Figure: +def make_figure(*args, **kwargs) -> plt.Figure: """ Create a new figure. @@ -45,15 +48,21 @@ def make_figure(*args, **kwargs) -> Figure: F) directly. When using the interactive backend, we need to do more work. The ``plt.Figure`` will not have a toolbar nor will it be interactive, as opposed to what - ``plt.figure`` returns. We therefore copy the minimal required code inside the - ``plt.figure`` function which creates a figure manager (which is apparently what - creates the toolbar and makes the figure interactive). + ``plt.figure`` returns. To fix this, we need to create a manager for the figure + (see https://stackoverflow.com/a/75477367). """ - if not is_interactive_backend(): - return Figure(*args, **kwargs) - backend = _get_backend_mod() - manager = backend.new_figure_manager(1, *args, FigureClass=Figure, **kwargs) - return manager.canvas.figure + fig = plt.Figure(*args, **kwargs) + if is_interactive_backend(): + # Create a manager for the figure, which makes it interactive, as well as + # making it possible to show the figure from the terminal. + plt._backend_mod.new_figure_manager_given_figure(1, fig) + return fig + # if not is_interactive_backend(): + # return fig # Figure(*args, **kwargs) + # # backend = _get_backend_mod() + # # manager = backend.new_figure_manager(1, *args, FigureClass=Figure, **kwargs) + # else: + # return manager.canvas.figure def make_legend(leg: bool | tuple[float, float] | str) -> dict: From 68239e06254152bbdea99c2830059674101c0327 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 25 Sep 2024 21:28:23 +0200 Subject: [PATCH 2/5] cleanup --- src/plopp/backends/matplotlib/canvas.py | 6 ------ src/plopp/backends/matplotlib/figure.py | 6 ++++++ src/plopp/backends/matplotlib/utils.py | 12 +----------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/plopp/backends/matplotlib/canvas.py b/src/plopp/backends/matplotlib/canvas.py index 13ce4ab1..b06ce465 100644 --- a/src/plopp/backends/matplotlib/canvas.py +++ b/src/plopp/backends/matplotlib/canvas.py @@ -201,12 +201,6 @@ def save(self, filename: str, **kwargs): """ self.fig.savefig(filename, **{**{'bbox_inches': 'tight'}, **kwargs}) - def show(self): - """ - Make a call to Matplotlib's underlying ``show`` function. - """ - self.fig.show() - def set_axes(self, dims, units, dtypes): """ Set the axes dimensions and units. diff --git a/src/plopp/backends/matplotlib/figure.py b/src/plopp/backends/matplotlib/figure.py index 2ee9a2b1..a49713ce 100644 --- a/src/plopp/backends/matplotlib/figure.py +++ b/src/plopp/backends/matplotlib/figure.py @@ -97,6 +97,12 @@ def copy(self, ax: Axes | None = None) -> MplBaseFig: setattr(out.canvas, prop, getattr(self.canvas, prop)) return out + def show(self): + """ + Make a call to Matplotlib's underlying ``show`` function. + """ + self.fig.show() + def _make_png_repr(fig): return {'image/png': fig_to_bytes(fig, form='png')} diff --git a/src/plopp/backends/matplotlib/utils.py b/src/plopp/backends/matplotlib/utils.py index d02e47bf..d0ba9b63 100644 --- a/src/plopp/backends/matplotlib/utils.py +++ b/src/plopp/backends/matplotlib/utils.py @@ -5,8 +5,6 @@ from typing import Literal import matplotlib as mpl - -# from matplotlib.pyplot import Figure, _backend_mod, _get_backend_mod import matplotlib.pyplot as plt @@ -32,9 +30,7 @@ def is_interactive_backend() -> bool: Return ``True`` if the current backend used by Matplotlib is the widget/ipympl backend. """ - backend = mpl.get_backend() - # return any(x in backend for x in ("ipympl", "widget")) - return "inline" not in backend + return "inline" not in mpl.get_backend() def make_figure(*args, **kwargs) -> plt.Figure: @@ -57,12 +53,6 @@ def make_figure(*args, **kwargs) -> plt.Figure: # making it possible to show the figure from the terminal. plt._backend_mod.new_figure_manager_given_figure(1, fig) return fig - # if not is_interactive_backend(): - # return fig # Figure(*args, **kwargs) - # # backend = _get_backend_mod() - # # manager = backend.new_figure_manager(1, *args, FigureClass=Figure, **kwargs) - # else: - # return manager.canvas.figure def make_legend(leg: bool | tuple[float, float] | str) -> dict: From 56092caf94b51c50ab84acb9d2c1f509cb158359 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 25 Sep 2024 23:12:36 +0200 Subject: [PATCH 3/5] fix old tests and is_interactive_backend --- src/plopp/backends/matplotlib/utils.py | 17 ++- tests/high_level_test.py | 188 ++++++++++++------------- 2 files changed, 107 insertions(+), 98 deletions(-) diff --git a/src/plopp/backends/matplotlib/utils.py b/src/plopp/backends/matplotlib/utils.py index d0ba9b63..a168651d 100644 --- a/src/plopp/backends/matplotlib/utils.py +++ b/src/plopp/backends/matplotlib/utils.py @@ -27,10 +27,16 @@ def fig_to_bytes(fig: plt.Figure, form: Literal['png', 'svg'] = 'png') -> bytes: def is_interactive_backend() -> bool: """ - Return ``True`` if the current backend used by Matplotlib is the widget/ipympl - backend. + Return ``True`` if the current backend used by Matplotlib creates interactive + figures. See + https://matplotlib.org/stable/users/explain/figure/backends.html#the-builtin-backends + for a list of backends. """ - return "inline" not in mpl.get_backend() + backend = mpl.get_backend().lower() + return any( + b in backend + for b in ('qt', 'ipympl', 'gtk', 'tk', 'wx', 'nbagg', 'web', 'macosx', 'widget') + ) def make_figure(*args, **kwargs) -> plt.Figure: @@ -51,7 +57,10 @@ def make_figure(*args, **kwargs) -> plt.Figure: if is_interactive_backend(): # Create a manager for the figure, which makes it interactive, as well as # making it possible to show the figure from the terminal. - plt._backend_mod.new_figure_manager_given_figure(1, fig) + try: + plt._backend_mod.new_figure_manager_given_figure(1, fig) + except AttributeError: + pass return fig diff --git a/tests/high_level_test.py b/tests/high_level_test.py index 9f26faa3..e12bfca3 100644 --- a/tests/high_level_test.py +++ b/tests/high_level_test.py @@ -2,6 +2,7 @@ # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) import ipywidgets as ipw +import pytest import scipp as sc from plopp import Node, imagefigure, linefigure, node, widget_node @@ -18,125 +19,124 @@ def hide_masks(data_array, masks): return out -def test_single_1d_line(): - da = data_array(ndim=1) - n = Node(da) - _ = linefigure(n) +@pytest.mark.usefixtures("_parametrize_all_backends") +class TestHighLevel1d: + def test_single_1d_line(self): + da = data_array(ndim=1) + n = Node(da) + _ = linefigure(n) + def test_two_1d_lines(self): + ds = dataset(ndim=1) + a = Node(ds['a']) + b = Node(ds['b']) + _ = linefigure(a, b) -def test_two_1d_lines(): - ds = dataset(ndim=1) - a = Node(ds['a']) - b = Node(ds['b']) - _ = linefigure(a, b) + def test_difference_of_two_1d_lines(self): + ds = dataset(ndim=1) + a = Node(ds['a']) + b = Node(ds['b']) + @node + def diff(x, y): + return x - y -def test_difference_of_two_1d_lines(): - ds = dataset(ndim=1) - a = Node(ds['a']) - b = Node(ds['b']) + c = diff(a, b) + _ = linefigure(a, b, c) - @node - def diff(x, y): - return x - y - c = diff(a, b) - _ = linefigure(a, b, c) +@pytest.mark.usefixtures("_parametrize_mpl_backends") +class TestHighLevel2d: + def test_2d_image(self): + da = data_array(ndim=2) + a = Node(da) + _ = imagefigure(a) -def test_2d_image(): - da = data_array(ndim=2) - a = Node(da) - _ = imagefigure(a) +@pytest.mark.usefixtures("_parametrize_interactive_2d_backends") +class TestHighLevelInteractive: + def test_2d_image_smoothing_slider(self): + da = data_array(ndim=2) + a = Node(da) + sl = ipw.IntSlider(min=1, max=10) + sigma_node = widget_node(sl) -def test_2d_image_smoothing_slider(): - da = data_array(ndim=2) - a = Node(da) + from scipp.scipy.ndimage import gaussian_filter - sl = ipw.IntSlider(min=1, max=10) - sigma_node = widget_node(sl) + smooth_node = Node(gaussian_filter, a, sigma=sigma_node) - from scipp.scipy.ndimage import gaussian_filter + fig = imagefigure(smooth_node) + Box([fig, sl]) + sl.value = 5 - smooth_node = Node(gaussian_filter, a, sigma=sigma_node) + def test_2d_image_with_masks(self): + da = data_array(ndim=2) + da.masks['m1'] = da.data < sc.scalar(0.0, unit='m/s') + da.masks['m2'] = da.coords['xx'] > sc.scalar(30.0, unit='m') - fig = imagefigure(smooth_node) - Box([fig.to_widget(), sl]) - sl.value = 5 + a = Node(da) + widget = Checkboxes(da.masks.keys()) + w = widget_node(widget) -def test_2d_image_with_masks(): - da = data_array(ndim=2) - da.masks['m1'] = da.data < sc.scalar(0.0, unit='m/s') - da.masks['m2'] = da.coords['xx'] > sc.scalar(30.0, unit='m') + masks_node = hide_masks(a, w) + fig = imagefigure(masks_node) + Box([fig, widget]) + widget.toggle_all_button.value = False - a = Node(da) + def test_two_1d_lines_with_masks(self): + ds = dataset() + ds['a'].masks['m1'] = ds['a'].coords['xx'] > sc.scalar(40.0, unit='m') + ds['a'].masks['m2'] = ds['a'].data < ds['b'].data + ds['b'].masks['m1'] = ds['b'].coords['xx'] < sc.scalar(5.0, unit='m') - widget = Checkboxes(da.masks.keys()) - w = widget_node(widget) + a = Node(ds['a']) + b = Node(ds['b']) - masks_node = hide_masks(a, w) - fig = imagefigure(masks_node) - Box([fig.to_widget(), widget]) - widget.toggle_all_button.value = False + widget = Checkboxes(list(ds['a'].masks.keys()) + list(ds['b'].masks.keys())) + w = widget_node(widget) + node_masks_a = hide_masks(a, w) + node_masks_b = hide_masks(b, w) + fig = linefigure(node_masks_a, node_masks_b) + Box([fig, widget]) + widget.toggle_all_button.value = False -def test_two_1d_lines_with_masks(): - ds = dataset() - ds['a'].masks['m1'] = ds['a'].coords['xx'] > sc.scalar(40.0, unit='m') - ds['a'].masks['m2'] = ds['a'].data < ds['b'].data - ds['b'].masks['m1'] = ds['b'].coords['xx'] < sc.scalar(5.0, unit='m') + def test_node_sum_data_along_y(self): + da = data_array(ndim=2, binedges=True) + a = Node(da) + s = Node(sc.sum, a, dim='yy') - a = Node(ds['a']) - b = Node(ds['b']) + fig1 = imagefigure(a) + fig2 = linefigure(s) + Box([[fig1, fig2]]) - widget = Checkboxes(list(ds['a'].masks.keys()) + list(ds['b'].masks.keys())) - w = widget_node(widget) + def test_slice_3d_cube(self): + da = data_array(ndim=3) + a = Node(da) + sl = SliceWidget(da, dims=['zz']) + w = widget_node(sl) - node_masks_a = hide_masks(a, w) - node_masks_b = hide_masks(b, w) - fig = linefigure(node_masks_a, node_masks_b) - Box([fig.to_widget(), widget]) - widget.toggle_all_button.value = False + slice_node = slice_dims(a, w) + + fig = imagefigure(slice_node) + Box([fig, sl]) + sl.controls["zz"]["slider"].value = 10 + def test_3d_image_slicer_with_connected_side_histograms(self): + da = data_array(ndim=3) + a = Node(da) + sl = SliceWidget(da, dims=['zz']) + w = widget_node(sl) -def test_node_sum_data_along_y(): - da = data_array(ndim=2, binedges=True) - a = Node(da) - s = Node(sc.sum, a, dim='yy') + sliced = slice_dims(a, w) + fig = imagefigure(sliced) - fig1 = imagefigure(a) - fig2 = linefigure(s) - Box([[fig1.to_widget(), fig2.to_widget()]]) + histx = Node(sc.sum, sliced, dim='xx') + histy = Node(sc.sum, sliced, dim='yy') - -def test_slice_3d_cube(): - da = data_array(ndim=3) - a = Node(da) - sl = SliceWidget(da, dims=['zz']) - w = widget_node(sl) - - slice_node = slice_dims(a, w) - - fig = imagefigure(slice_node) - Box([fig.to_widget(), sl]) - sl.controls["zz"]["slider"].value = 10 - - -def test_3d_image_slicer_with_connected_side_histograms(): - da = data_array(ndim=3) - a = Node(da) - sl = SliceWidget(da, dims=['zz']) - w = widget_node(sl) - - sliced = slice_dims(a, w) - fig = imagefigure(sliced) - - histx = Node(sc.sum, sliced, dim='xx') - histy = Node(sc.sum, sliced, dim='yy') - - fx = linefigure(histx) - fy = linefigure(histy) - Box([[fx.to_widget(), fy.to_widget()], fig.to_widget(), sl]) - sl.controls["zz"]["slider"].value = 10 + fx = linefigure(histx) + fy = linefigure(histy) + Box([[fx, fy], fig, sl]) + sl.controls["zz"]["slider"].value = 10 From 3868b2a99a6ca9401a12a19606fce27697637324 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Thu, 26 Sep 2024 07:43:47 +0200 Subject: [PATCH 4/5] remove plopp.show from API reference --- docs/api-reference/index.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index c3dd2039..7195de8a 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -92,12 +92,3 @@ plotly pythreejs ``` - -## Other - -```{eval-rst} -.. autosummary:: - :toctree: ../generated - - show -``` From 06919ae857ae22d73083fa0816eb2d6644e85d4b Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Thu, 26 Sep 2024 09:59:35 +0200 Subject: [PATCH 5/5] remove try/except and add notebook to list of backends --- src/plopp/backends/matplotlib/utils.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/plopp/backends/matplotlib/utils.py b/src/plopp/backends/matplotlib/utils.py index a168651d..1818a878 100644 --- a/src/plopp/backends/matplotlib/utils.py +++ b/src/plopp/backends/matplotlib/utils.py @@ -35,7 +35,18 @@ def is_interactive_backend() -> bool: backend = mpl.get_backend().lower() return any( b in backend - for b in ('qt', 'ipympl', 'gtk', 'tk', 'wx', 'nbagg', 'web', 'macosx', 'widget') + for b in ( + 'qt', + 'ipympl', + 'gtk', + 'tk', + 'wx', + 'nbagg', + 'web', + 'macosx', + 'widget', + 'notebook', + ) ) @@ -57,10 +68,7 @@ def make_figure(*args, **kwargs) -> plt.Figure: if is_interactive_backend(): # Create a manager for the figure, which makes it interactive, as well as # making it possible to show the figure from the terminal. - try: - plt._backend_mod.new_figure_manager_given_figure(1, fig) - except AttributeError: - pass + plt._backend_mod.new_figure_manager_given_figure(1, fig) return fig