From db6badbc6e3947055e7ba6001e026e49302e4de6 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 14 Sep 2026 12:02:09 +0200 Subject: [PATCH 1/2] add option to hide interactive log buttons --- src/plopp/backends/matplotlib/canvas.py | 44 ++++++++++++-------- src/plopp/graphics/colormapper.py | 13 +++++- src/plopp/graphics/graphicalview.py | 3 ++ src/plopp/plotting/_inspector.py | 5 +++ src/plopp/plotting/_mesh3d.py | 4 ++ src/plopp/plotting/_plot.py | 4 ++ src/plopp/plotting/_scatter.py | 4 ++ src/plopp/plotting/_scatter3d.py | 4 ++ src/plopp/plotting/_slicer.py | 4 ++ src/plopp/plotting/_superplot.py | 4 ++ src/plopp/plotting/_xyplot.py | 4 ++ src/plopp/widgets/hoverbutton.py | 54 +++++++++++++++++++++++++ 12 files changed, 128 insertions(+), 19 deletions(-) diff --git a/src/plopp/backends/matplotlib/canvas.py b/src/plopp/backends/matplotlib/canvas.py index ea9ff8e6..95791e43 100644 --- a/src/plopp/backends/matplotlib/canvas.py +++ b/src/plopp/backends/matplotlib/canvas.py @@ -167,6 +167,8 @@ class Canvas: The label for the y axis. norm: Set to ``'log'`` for a logarithmic y-axis (legacy, prefer ``logy`` instead). + hide_log_buttons: + If ``True``, hide the buttons for toggling logarithmic scales on the axes. """ def __init__( @@ -191,6 +193,7 @@ def __init__( ylabel: str | None = None, norm: Literal['linear', 'log'] | None = None, autoscale_axes: Callable | None = None, + hide_log_buttons: bool = False, **ignored, ): # Note on the `**ignored`` keyword arguments: the figure which owns the canvas @@ -256,26 +259,33 @@ def __init__( self.fig.canvas.toolbar_visible = False self.fig.canvas.header_visible = False - args = {"transform": self.ax.transAxes, "ha": "right", "va": "top"} - self._logx_button = CanvasToggleButton( - ax=self.ax, label="logX", position=(0.985, -0.02), **args - ) - self._logy_button = CanvasToggleButton( - ax=self.ax, label="logY", position=(-0.015, 0.98), **args - ) - - if self.cax is not None: - args = {"transform": self.cax.transAxes, "ha": "center"} - self._logc_button = CanvasToggleButton( - ax=self.cax, label="log", position=(0.5, 0.98), va="top", **args + if not hide_log_buttons: + args = {"transform": self.ax.transAxes, "ha": "right", "va": "top"} + self._logx_button = CanvasToggleButton( + ax=self.ax, label="logX", position=(0.985, -0.02), **args ) - self._fitc_button = CanvasToggleButton( - ax=self.cax, label="fit", position=(0.5, 0.02), va="bottom", **args + self._logy_button = CanvasToggleButton( + ax=self.ax, label="logY", position=(-0.015, 0.98), **args ) - self.fig.canvas.mpl_connect("figure_enter_event", self._on_mouse_enter) - self.fig.canvas.mpl_connect("figure_leave_event", self._on_mouse_leave) - self.fig.canvas.mpl_connect("button_press_event", self._on_log_button_click) + if self.cax is not None: + args = {"transform": self.cax.transAxes, "ha": "center"} + self._logc_button = CanvasToggleButton( + ax=self.cax, label="log", position=(0.5, 0.98), va="top", **args + ) + self._fitc_button = CanvasToggleButton( + ax=self.cax, + label="fit", + position=(0.5, 0.02), + va="bottom", + **args, + ) + + self.fig.canvas.mpl_connect("figure_enter_event", self._on_mouse_enter) + self.fig.canvas.mpl_connect("figure_leave_event", self._on_mouse_leave) + self.fig.canvas.mpl_connect( + "button_press_event", self._on_log_button_click + ) if logx: self.xscale = 'log' diff --git a/src/plopp/graphics/colormapper.py b/src/plopp/graphics/colormapper.py index c7e295c1..62be34bd 100644 --- a/src/plopp/graphics/colormapper.py +++ b/src/plopp/graphics/colormapper.py @@ -136,6 +136,8 @@ class ColorMapper: The maximum value for the colorscale range. If a number (without a unit) is supplied, it is assumed that the unit is the same as the data unit. This is an old parameter name. Prefer using ``cmax`` instead. + hide_log_buttons: + If ``True``, the interactive log buttons will be hidden. """ def __init__( @@ -154,6 +156,7 @@ def __init__( norm: Literal['linear', 'log'] | None = None, vmin: sc.Variable | float | None = None, vmax: sc.Variable | float | None = None, + hide_log_buttons: bool = False, ): cmin = parse_mutually_exclusive(vmin=vmin, cmin=cmin) cmax = parse_mutually_exclusive(vmax=vmax, cmax=cmax) @@ -193,6 +196,7 @@ def __init__( self.changed = False self.artists = {} self.widget = None + self._hide_log_buttons = hide_log_buttons if cbar: if self.cax is None: @@ -223,11 +227,16 @@ def to_widget(self): """ Convert the colorbar into a widget for use with other ``ipywidgets``. """ - from ..widgets.hoverbutton import HoverButtonWidget + from ..widgets.hoverbutton import HoverButtonWidget, PlainImageWidget + + if self._hide_log_buttons: + self.widget = PlainImageWidget() + self._update_colorbar_widget() + return self.widget self.widget = HoverButtonWidget(log_value=self._logc) - self._update_colorbar_widget() self.widget.on_log_button_click(self.toggle_norm) + self._update_colorbar_widget() def fit(): self.autoscale() diff --git a/src/plopp/graphics/graphicalview.py b/src/plopp/graphics/graphicalview.py index 3dbf6d18..074ecf4b 100644 --- a/src/plopp/graphics/graphicalview.py +++ b/src/plopp/graphics/graphicalview.py @@ -83,6 +83,7 @@ def __init__( zlabel: str | None = None, clabel: str | None = None, nan_color: str | None = None, + hide_log_buttons: bool = False, **kwargs, ): super().__init__(*nodes) @@ -123,6 +124,7 @@ def __init__( zlabel=zlabel, norm=norm if len(dims) == 1 else None, autoscale_axes=self.autoscale, + hide_log_buttons=hide_log_buttons, ) if colormapper: @@ -141,6 +143,7 @@ def __init__( canvas=self.canvas, figsize=getattr(self.canvas, "figsize", None), nan_color=nan_color, + hide_log_buttons=hide_log_buttons, ) self._kwargs['colormapper'] = self.colormapper if self._autoscale: diff --git a/src/plopp/plotting/_inspector.py b/src/plopp/plotting/_inspector.py index bd02d11d..184d07d6 100644 --- a/src/plopp/plotting/_inspector.py +++ b/src/plopp/plotting/_inspector.py @@ -148,6 +148,7 @@ def inspector( errorbars: Literal['band', 'bar', True, False] = True, figsize: tuple[float, float] | None = None, grid: bool = False, + hide_log_buttons: bool = False, legend: bool | tuple[float, float] = True, logc: bool | None = None, mask_cmap: str = 'gray', @@ -252,6 +253,8 @@ def inspector( The width and height of the figure, in inches. grid: Show grid if ``True``. + hide_log_buttons: + If ``True``, the interactive log buttons will be hidden. legend: Show legend if ``True``. If ``legend`` is a tuple, it should contain the ``(x, y)`` coordinates of the legend's anchor point in axes coordinates @@ -318,6 +321,7 @@ def inspector( autoscale=autoscale, errorbars=errorbars, grid=grid, + hide_log_buttons=hide_log_buttons, legend=legend, mask_color=mask_color, xmax=xmax, @@ -357,6 +361,7 @@ def inspector( cmin=cmin, figsize=figsize, grid=grid, + hide_log_buttons=hide_log_buttons, logc=logc, mask_cmap=mask_cmap, mask_color=mask_color, diff --git a/src/plopp/plotting/_mesh3d.py b/src/plopp/plotting/_mesh3d.py index 1c77d231..07faa9f5 100644 --- a/src/plopp/plotting/_mesh3d.py +++ b/src/plopp/plotting/_mesh3d.py @@ -51,6 +51,7 @@ def mesh3d( cmin: sc.Variable | float = None, edgecolor: str | None = None, figsize: tuple[int, int] = (600, 400), + hide_log_buttons: bool = False, logc: bool | None = None, nan_color: str | None = None, norm: Literal['linear', 'log'] | None = None, @@ -90,6 +91,8 @@ def mesh3d( The color of the edges. If None, no edges are drawn. figsize: The size of the 3d rendering area, in pixels: ``(width, height)``. + hide_log_buttons: + If ``True``, the interactive log buttons will be hidden. logc: Set to ``True`` for a logarithmic colorscale (only applicable if ``cbar`` is ``True``). @@ -128,6 +131,7 @@ def mesh3d( cmap=cmap, edgecolor=edgecolor, figsize=figsize, + hide_log_buttons=hide_log_buttons, logc=logc, nan_color=nan_color, norm=norm, diff --git a/src/plopp/plotting/_plot.py b/src/plopp/plotting/_plot.py index 5ce30351..59f4a9ec 100644 --- a/src/plopp/plotting/_plot.py +++ b/src/plopp/plotting/_plot.py @@ -32,6 +32,7 @@ def plot( grid: bool = False, ignore_size: bool = False, legend: bool | tuple[float, float] = True, + hide_log_buttons: bool = False, logc: bool | None = None, logx: bool | None = None, logy: bool | None = None, @@ -80,6 +81,8 @@ def plot( The width and height of the figure, in inches. grid: Show grid if ``True``. + hide_log_buttons: + If ``True``, the interactive log buttons will be hidden. ignore_size: If ``True``, skip the check that prevents the rendering of very large data. legend: @@ -144,6 +147,7 @@ def plot( errorbars=errorbars, figsize=figsize, grid=grid, + hide_log_buttons=hide_log_buttons, legend=legend, logc=logc, logx=logx, diff --git a/src/plopp/plotting/_scatter.py b/src/plopp/plotting/_scatter.py index f5708dc5..8714a4a1 100644 --- a/src/plopp/plotting/_scatter.py +++ b/src/plopp/plotting/_scatter.py @@ -56,6 +56,7 @@ def scatter( cmin: sc.Variable | float | None = None, figsize: tuple[float, float] | None = None, grid: bool = False, + hide_log_buttons: bool = False, ignore_size: bool = False, legend: bool | tuple[float, float] = True, logc: bool | None = None, @@ -126,6 +127,8 @@ def scatter( If ``True``, use logarithmic scale for y-axis. mask_color: Color of markers for masked data. + hide_log_buttons: + If ``True``, the interactive log buttons will be hidden. nan_color: Color to use for NaN values in color mapping (only applicable if ``cbar`` is ``True``). @@ -182,6 +185,7 @@ def scatter( logx=logx, logy=logy, mask_color=mask_color, + hide_log_buttons=hide_log_buttons, nan_color=nan_color, norm=norm, scale=scale, diff --git a/src/plopp/plotting/_scatter3d.py b/src/plopp/plotting/_scatter3d.py index bf8d3fd0..065738f6 100644 --- a/src/plopp/plotting/_scatter3d.py +++ b/src/plopp/plotting/_scatter3d.py @@ -52,6 +52,7 @@ def scatter3d( cmax: sc.Variable | float = None, cmin: sc.Variable | float = None, figsize: tuple[int, int] = (600, 400), + hide_log_buttons: bool = False, logc: bool | None = None, nan_color: str | None = None, norm: Literal['linear', 'log'] | None = None, @@ -117,6 +118,8 @@ def scatter3d( perspective: Set to ``True`` for a perspective camera. ``False`` will give an orthographic (flat) camera. + hide_log_buttons: + If ``True``, the interactive log buttons will be hidden. title: The figure title. vmin: @@ -161,6 +164,7 @@ def scatter3d( nan_color=nan_color, norm=norm, opacity=opacity, + hide_log_buttons=hide_log_buttons, perspective=perspective, title=title, vmax=vmax, diff --git a/src/plopp/plotting/_slicer.py b/src/plopp/plotting/_slicer.py index 01ca6d5e..b98a00ef 100644 --- a/src/plopp/plotting/_slicer.py +++ b/src/plopp/plotting/_slicer.py @@ -260,6 +260,7 @@ def slicer( errorbars: Literal['band', 'bar', True, False] = True, figsize: tuple[float, float] | None = None, grid: bool = False, + hide_log_buttons: bool = False, legend: bool | tuple[float, float] = True, logc: bool | None = None, logx: bool | None = None, @@ -322,6 +323,8 @@ def slicer( The width and height of the figure, in inches. grid: Show grid if ``True``. + hide_log_buttons: + If ``True``, the interactive log buttons will be hidden. legend: Show legend if ``True``. If ``legend`` is a tuple, it should contain the ``(x, y)`` coordinates of the legend's anchor point in axes coordinates. @@ -391,6 +394,7 @@ def slicer( errorbars=errorbars, figsize=figsize, grid=grid, + hide_log_buttons=hide_log_buttons, legend=legend, logc=logc, logx=logx, diff --git a/src/plopp/plotting/_superplot.py b/src/plopp/plotting/_superplot.py index 902cfc14..8b56bdc0 100644 --- a/src/plopp/plotting/_superplot.py +++ b/src/plopp/plotting/_superplot.py @@ -21,6 +21,7 @@ def superplot( errorbars: Literal['band', 'bar', True, False] = True, figsize: tuple[float, float] | None = None, grid: bool = False, + hide_log_buttons: bool = False, legend: bool | tuple[float, float] = True, logx: bool | None = None, logy: bool | None = None, @@ -68,6 +69,8 @@ def superplot( The width and height of the figure, in inches. grid: Show grid if ``True``. + hide_log_buttons: + If ``True``, the interactive log buttons will be hidden. legend: Show legend if ``True``. If ``legend`` is a tuple, it should contain the ``(x, y)`` coordinates of the legend's anchor point in axes coordinates. @@ -124,6 +127,7 @@ def superplot( legend=legend, logx=logx, logy=logy, + hide_log_buttons=hide_log_buttons, mask_color=mask_color, norm=norm, scale=scale, diff --git a/src/plopp/plotting/_xyplot.py b/src/plopp/plotting/_xyplot.py index 54f24bcf..4f3f285e 100644 --- a/src/plopp/plotting/_xyplot.py +++ b/src/plopp/plotting/_xyplot.py @@ -45,6 +45,7 @@ def xyplot( errorbars: Literal['band', 'bar', True, False] = True, figsize: tuple[float, float] | None = None, grid: bool = False, + hide_log_buttons: bool = False, legend: bool | tuple[float, float] = True, logx: bool | None = None, logy: bool | None = None, @@ -84,6 +85,8 @@ def xyplot( The width and height of the figure, in inches. grid: Show grid if ``True``. + hide_log_buttons: + If ``True``, the interactive log buttons will be hidden. legend: Show legend if ``True``. If ``legend`` is a tuple, it should contain the ``(x, y)`` coordinates of the legend's anchor point in axes coordinates. @@ -127,6 +130,7 @@ def xyplot( errorbars=errorbars, figsize=figsize, grid=grid, + hide_log_buttons=hide_log_buttons, legend=legend, logx=logx, logy=logy, diff --git a/src/plopp/widgets/hoverbutton.py b/src/plopp/widgets/hoverbutton.py index 995d82f4..97d1df1a 100644 --- a/src/plopp/widgets/hoverbutton.py +++ b/src/plopp/widgets/hoverbutton.py @@ -148,3 +148,57 @@ def on_fit_button_click(self, handler): def set_svg(self, svg_string): """Set SVG from a string""" self.svg_data = svg_string + + +class PlainImageWidget(anywidget.AnyWidget): + """ + A custom widget that displays an SVG. + """ + + _esm = """ + function render({ model, el }) { + // Create container + const container = document.createElement('div'); + container.style.position = 'relative'; + container.style.display = 'inline-block'; + container.style.height = '98%'; + + // Create SVG container + const svgContainer = document.createElement('div'); + svgContainer.style.width = '100%'; + svgContainer.style.lineHeight = '0'; + + // Function to update SVG + function updateSVG() { + const svgData = new TextDecoder().decode(model.get('svg_data')); + svgContainer.innerHTML = svgData; + const svg = svgContainer.querySelector('svg'); + if (svg) { + svg.style.width = '100%'; + svg.style.height = 'auto'; + svg.style.display = 'block'; + } + } + + // Initial SVG + updateSVG(); + + // Listen for SVG changes + model.on('change:svg_data', updateSVG); + + // Assemble widget + container.appendChild(svgContainer); + el.appendChild(container); + } + export default { render }; + """ + + # Traitlets + svg_data = traitlets.Bytes(b'').tag(sync=True) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def set_svg(self, svg_string): + """Set SVG from a string""" + self.svg_data = svg_string From ca050abefac174ea79e2f1f6ec63b4ffd282515d Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 14 Sep 2026 12:10:20 +0200 Subject: [PATCH 2/2] add tests --- tests/plotting/inspector_test.py | 7 +++++++ tests/plotting/mesh3d_test.py | 11 +++++++++++ tests/plotting/plot_1d_test.py | 6 ++++++ tests/plotting/plot_2d_test.py | 6 ++++++ tests/plotting/scatter3d_test.py | 6 ++++++ tests/plotting/scatter_test.py | 6 ++++++ tests/plotting/slicer_test.py | 10 ++++++++++ tests/plotting/superplot_test.py | 6 ++++++ tests/plotting/xyplot_test.py | 7 +++++++ 9 files changed, 65 insertions(+) diff --git a/tests/plotting/inspector_test.py b/tests/plotting/inspector_test.py index bf4e46d7..64afd088 100644 --- a/tests/plotting/inspector_test.py +++ b/tests/plotting/inspector_test.py @@ -428,3 +428,10 @@ def test_use_non_dimension_coord_as_slice_dim(): coords={'tof': tof, 'y': y, 'x': x, 'wavelengths': wavelengths}, ) pp.inspector(array, dim='tof') + + +@pytest.mark.usefixtures('_use_ipympl') +@pytest.mark.parametrize("hide_log_buttons", [True, False]) +def test_hide_log_buttons(hide_log_buttons): + da = pp.data.data3d() + pp.inspector(da, hide_log_buttons=hide_log_buttons) diff --git a/tests/plotting/mesh3d_test.py b/tests/plotting/mesh3d_test.py index 89365143..a9c9f5d0 100644 --- a/tests/plotting/mesh3d_test.py +++ b/tests/plotting/mesh3d_test.py @@ -2,6 +2,7 @@ # Copyright (c) 2024 Scipp contributors (https://github.com/scipp) import numpy as np +import pytest import plopp as pp from plopp.data import examples @@ -61,3 +62,13 @@ def test_mesh3d_cmap(): cmap='magma', ) assert fig.view.colormapper.cmap.name == 'magma' + + +@pytest.mark.parametrize("hide_log_buttons", [True, False]) +def test_mesh3d_hide_log_buttons(hide_log_buttons): + teapot_data = examples.teapot() + pp.mesh3d( + vertices=teapot_data["vertices"], + faces=teapot_data["faces"], + hide_log_buttons=hide_log_buttons, + ) diff --git a/tests/plotting/plot_1d_test.py b/tests/plotting/plot_1d_test.py index 19d4fe2e..ee116175 100644 --- a/tests/plotting/plot_1d_test.py +++ b/tests/plotting/plot_1d_test.py @@ -730,3 +730,9 @@ def test_plot_data_with_all_nans_does_not_raise(): da = data_array(ndim=1) da.values[...] = np.nan _ = da.plot() + + +@pytest.mark.parametrize("hide_log_buttons", [True, False]) +def test_hide_log_buttons(hide_log_buttons): + da = data_array(ndim=1) + da.plot(hide_log_buttons=hide_log_buttons) diff --git a/tests/plotting/plot_2d_test.py b/tests/plotting/plot_2d_test.py index 38eca59a..5c19083e 100644 --- a/tests/plotting/plot_2d_test.py +++ b/tests/plotting/plot_2d_test.py @@ -533,3 +533,9 @@ def test_plot_data_with_all_nans_does_not_raise(): da = data_array(ndim=2) da.values[...] = np.nan _ = da.plot() + + +@pytest.mark.parametrize("hide_log_buttons", [True, False]) +def test_hide_log_buttons(hide_log_buttons): + da = data_array(ndim=2) + da.plot(hide_log_buttons=hide_log_buttons) diff --git a/tests/plotting/scatter3d_test.py b/tests/plotting/scatter3d_test.py index 3dd1d039..5793f667 100644 --- a/tests/plotting/scatter3d_test.py +++ b/tests/plotting/scatter3d_test.py @@ -166,3 +166,9 @@ def test_figure_has_only_unit_on_colorbar_for_multiple_sets_of_scatter_points(): def test_scatter3d_no_perspective(): da = scatter() pp.scatter3d(da, perspective=False) + + +@pytest.mark.parametrize("hide_log_buttons", [True, False]) +def test_scatter3d_hide_log_buttons(hide_log_buttons): + da = scatter() + pp.scatter3d(da, hide_log_buttons=hide_log_buttons) diff --git a/tests/plotting/scatter_test.py b/tests/plotting/scatter_test.py index 031abb95..0a4afd7b 100644 --- a/tests/plotting/scatter_test.py +++ b/tests/plotting/scatter_test.py @@ -263,3 +263,9 @@ def test_clabel(): da = scatter_data() fig = pp.scatter(da, cbar=True, clabel='MyColorLabel') assert fig.view.colormapper.clabel == 'MyColorLabel' + + +@pytest.mark.parametrize("hide_log_buttons", [True, False]) +def test_hide_log_buttons(hide_log_buttons): + da = scatter_data() + pp.scatter(da, hide_log_buttons=hide_log_buttons) diff --git a/tests/plotting/slicer_test.py b/tests/plotting/slicer_test.py index 72d8267d..208b6512 100644 --- a/tests/plotting/slicer_test.py +++ b/tests/plotting/slicer_test.py @@ -247,6 +247,11 @@ def test_slicer_with_first_all_nan_slice_does_not_raise(self, mode): da['yy', 0].values[...] = np.nan SlicerPlot(da, keep=['xx'], mode=mode) + @pytest.mark.parametrize("hide_log_buttons", [True, False]) + def test_slicer_hide_log_buttons(self, hide_log_buttons): + da = data_array(ndim=3) + SlicerPlot(da, keep=['xx', 'yy'], hide_log_buttons=hide_log_buttons) + @pytest.mark.usefixtures("_parametrize_interactive_2d_backends") class TestSlicer2d: @@ -420,3 +425,8 @@ def test_slicer_with_first_all_nan_slice_does_not_raise(self, mode): da = data_array(ndim=3) da['zz', 0].values[...] = np.nan SlicerPlot(da, keep=['xx'], mode=mode) + + @pytest.mark.parametrize("hide_log_buttons", [True, False]) + def test_slicer_hide_log_buttons(self, hide_log_buttons): + da = data_array(ndim=3) + SlicerPlot(da, keep=['xx', 'yy'], hide_log_buttons=hide_log_buttons) diff --git a/tests/plotting/superplot_test.py b/tests/plotting/superplot_test.py index b928ec86..217a39db 100644 --- a/tests/plotting/superplot_test.py +++ b/tests/plotting/superplot_test.py @@ -81,3 +81,9 @@ def test_raises_ValueError_when_given_binned_data(): da = sc.data.table_xyz(100).bin(x=10, y=20) with pytest.raises(ValueError, match='Cannot plot binned data'): superplot(da, keep='x') + + +@pytest.mark.parametrize("hide_log_buttons", [True, False]) +def test_superplot_hide_log_buttons(hide_log_buttons): + da = data_array(ndim=2) + superplot(da, keep='xx', hide_log_buttons=hide_log_buttons) diff --git a/tests/plotting/xyplot_test.py b/tests/plotting/xyplot_test.py index a4ff31af..944ef68f 100644 --- a/tests/plotting/xyplot_test.py +++ b/tests/plotting/xyplot_test.py @@ -88,3 +88,10 @@ def test_xyplot_from_nodes(): pp.xyplot(pp.Node(x), y) pp.xyplot(x, pp.Node(y)) pp.xyplot(pp.Node(x), pp.Node(y)) + + +@pytest.mark.parametrize("hide_log_buttons", [True, False]) +def test_xyplot_hide_log_buttons(hide_log_buttons): + x = sc.arange('time', 20.0, unit='s') + y = sc.arange('time', 100.0, 120.0, unit='K') + pp.xyplot(x, y, hide_log_buttons=hide_log_buttons)