From da24351fcf93b40c135e337f0ae54341dfe7b6bc Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 10 Jun 2026 16:51:42 +0200 Subject: [PATCH 1/4] remove plotly backend --- src/plopp/backends/plotly/__init__.py | 2 - src/plopp/backends/plotly/canvas.py | 373 ---------------------- src/plopp/backends/plotly/figure.py | 44 --- src/plopp/backends/plotly/line.py | 319 ------------------ tests/backends/plotly/conftest.py | 13 - tests/backends/plotly/plotly_line_test.py | 114 ------- tests/conftest.py | 5 +- tests/graphics/artists_test.py | 17 - tests/graphics/canvas_test.py | 7 - tests/graphics/figures_test.py | 7 - tests/plotting/plot_1d_test.py | 18 -- 11 files changed, 2 insertions(+), 917 deletions(-) delete mode 100644 src/plopp/backends/plotly/__init__.py delete mode 100644 src/plopp/backends/plotly/canvas.py delete mode 100644 src/plopp/backends/plotly/figure.py delete mode 100644 src/plopp/backends/plotly/line.py delete mode 100644 tests/backends/plotly/conftest.py delete mode 100644 tests/backends/plotly/plotly_line_test.py diff --git a/src/plopp/backends/plotly/__init__.py b/src/plopp/backends/plotly/__init__.py deleted file mode 100644 index 012cd010c..000000000 --- a/src/plopp/backends/plotly/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2024 Scipp contributors (https://github.com/scipp) diff --git a/src/plopp/backends/plotly/canvas.py b/src/plopp/backends/plotly/canvas.py deleted file mode 100644 index 0c704ef70..000000000 --- a/src/plopp/backends/plotly/canvas.py +++ /dev/null @@ -1,373 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) - -from typing import Literal - -import plotly.graph_objects as go -import scipp as sc - -from ...core.utils import maybe_variable_to_number -from ...graphics.bbox import BoundingBox -from ...utils import parse_mutually_exclusive - - -class Canvas: - """ - Plotly-based canvas used to render 2D graphics. - It provides a figure and some axes, as well as functions for controlling the zoom, - panning, and the scale of the axes. - - Parameters - ---------- - figsize: - The width and height of the figure, in inches. - title: - The title to be placed above the figure. - user_vmin: - The minimum value for the vertical axis. If a number (without a unit) is - supplied, it is assumed that the unit is the same as the current vertical axis - unit. - user_vmax: - The maximum value for the vertical axis. If a number (without a unit) is - supplied, it is assumed that the unit is the same as the current vertical axis - unit. - """ - - def __init__( - self, - figsize: tuple[float, float] | None = None, - title: str | None = None, - user_vmin: sc.Variable | float | None = None, - user_vmax: sc.Variable | float | None = None, - xmin: sc.Variable | float | None = None, - xmax: sc.Variable | float | None = None, - ymin: sc.Variable | float | None = None, - ymax: sc.Variable | float | None = None, - logx: bool | None = None, - logy: bool | None = None, - xlabel: str | None = None, - ylabel: str | None = None, - norm: Literal['linear', 'log'] | None = None, - **ignored, - ): - # Note on the `**ignored`` keyword arguments: the figure which owns the canvas - # creates both the canvas and an artist object (Line or Image). The figure - # accepts keyword arguments, and has to somehow forward them to the canvas and - # the artist. Since the figure has no detailed knowledge of the underlying - # backend that implements the canvas, it cannot have specific arguments (such - # as `layout` for specifying a Plotly layout). - # Instead, we forward all the kwargs from the figure to both the canvas and the - # artist, and filter out the artist kwargs with `**ignored`. - - ymin = parse_mutually_exclusive(vmin=user_vmin, ymin=ymin) - ymax = parse_mutually_exclusive(vmax=user_vmax, ymax=ymax) - logy = parse_mutually_exclusive(norm=norm, logy=logy) - - self.fig = go.FigureWidget( - layout={ - 'modebar_remove': [ - 'zoom', - 'pan', - 'select', - 'toImage', - 'zoomIn', - 'zoomOut', - 'autoScale', - 'resetScale', - 'lasso2d', - ], - 'margin': {'l': 0, 'r': 0, 't': 0 if title is None else 40, 'b': 0}, - 'dragmode': False, - 'width': 600 if figsize is None else figsize[0], - 'height': 400 if figsize is None else figsize[1], - } - ) - self.figsize = figsize - self._xmin = xmin - self._xmax = xmax - self._ymin = ymin - self._ymax = ymax - self._xlabel = xlabel - self._ylabel = ylabel - self.units = {} - self.dims = {} - self._own_axes = False - if title: - self.title = title - self.bbox = BoundingBox() - - logx = False if logx is None else logx - logy = False if logy is None else logy - if logx: - self.xscale = 'log' - if logy: - self.yscale = 'log' - if xlabel is not None: - self.xlabel = xlabel - if ylabel is not None: - self.ylabel = ylabel - - def to_widget(self): - return self.fig - - def save(self, filename: str): - """ - Save the figure to file. - The default directory for writing the file is the same as the - directory where the script or notebook is running. - - Parameters - ---------- - filename: - Name of the output file. Possible file extensions are ``.jpg``, ``.png``, - ``.svg``, ``.pdf``, and ``.html`. - """ - ext = filename.split('.')[-1] - if ext == 'html': - self.fig.write_html(filename) - else: - self.fig.write_image(filename) - - def set_axes(self, dims, units, dtypes): - """ - Set the axes dimensions and units. - - Parameters - ---------- - dims: - The dimensions of the data. - units: - The units of the data. - dtypes: - The data types of the data. - """ - self.dims = dims - self.units = units - self.dtypes = dtypes - key = 'y' if 'y' in self.units else 'data' - self.bbox = BoundingBox( - xmin=maybe_variable_to_number(self._xmin, unit=self.units['x']), - xmax=maybe_variable_to_number(self._xmax, unit=self.units['x']), - ymin=maybe_variable_to_number(self._ymin, unit=self.units[key]), - ymax=maybe_variable_to_number(self._ymax, unit=self.units[key]), - ) - - @property - def empty(self) -> bool: - """ - Check if the canvas is empty. - """ - return not self.dims - - @property - def title(self) -> str: - """ - Get or set the title of the plot. - """ - return self.fig.layout.title.text - - @title.setter - def title(self, text: str): - layout = self.fig.layout - if not text: - layout.margin.t = 0 - elif layout.margin.t == 0: - layout.margin.t = 40 - layout.title = text - - @property - def xlabel(self) -> str: - """ - Get or set the label of the x-axis. - """ - return self.fig.layout.xaxis.title.text - - @xlabel.setter - def xlabel(self, lab: str): - self.fig.layout.xaxis.title = lab - - @property - def ylabel(self) -> str: - """ - Get or set the label of the y-axis. - """ - return self.fig.layout.yaxis.title.text - - @ylabel.setter - def ylabel(self, lab: str): - self.fig.layout.yaxis.title = lab - - @property - def xscale(self) -> Literal['linear', 'log']: - """ - Get or set the scale of the x-axis ('linear' or 'log'). - """ - return self.fig.layout.xaxis.type or 'linear' - - @xscale.setter - def xscale(self, scale: Literal['linear', 'log']): - self.fig.update_xaxes(type=scale) - - @property - def yscale(self) -> Literal['linear', 'log']: - """ - Get or set the scale of the y-axis ('linear' or 'log'). - """ - return self.fig.layout.yaxis.type or 'linear' - - @yscale.setter - def yscale(self, scale: Literal['linear', 'log']): - self.fig.update_yaxes(type=scale) - - @property - def xmin(self) -> float: - """ - Get or set the lower (left) bound of the x-axis. - """ - return self.fig.layout.xaxis.range[0] - - @xmin.setter - def xmin(self, value: float): - self.fig.layout.xaxis.range = [value, self.xmax] - - @property - def xmax(self) -> float: - """ - Get or set the upper (right) bound of the x-axis. - """ - return self.fig.layout.xaxis.range[1] - - @xmax.setter - def xmax(self, value: float): - self.fig.layout.xaxis.range = [self.xmin, value] - - @property - def xrange(self) -> tuple[float, float]: - """ - Get or set the range/limits of the x-axis. - """ - return self.fig.layout.xaxis.range - - @xrange.setter - def xrange(self, value: tuple[float, float]): - self.fig.layout.xaxis.range = value - - @property - def ymin(self) -> float: - """ - Get or set the lower (bottom) bound of the y-axis. - """ - return self.fig.layout.yaxis.range[0] - - @ymin.setter - def ymin(self, value: float): - self.fig.layout.yaxis.range = [value, self.ymax] - - @property - def ymax(self) -> float: - """ - Get or set the upper (top) bound of the y-axis. - """ - return self.fig.layout.yaxis.range[1] - - @ymax.setter - def ymax(self, value: float): - self.fig.layout.yaxis.range = [self.ymin, value] - - @property - def yrange(self) -> tuple[float, float]: - """ - Get or set the range/limits of the y-axis. - """ - return self.fig.layout.yaxis.range - - @yrange.setter - def yrange(self, value: tuple[float, float]): - self.fig.layout.yaxis.range = value - - @property - def logx(self) -> bool: - """ - Get or set whether the x-axis is in logarithmic scale. - """ - return self.xscale == 'log' - - @logx.setter - def logx(self, value: bool): - self.xscale = 'log' if value else 'linear' - - @property - def logy(self) -> bool: - """ - Get or set whether the y-axis is in logarithmic scale. - """ - return self.yscale == 'log' - - @logy.setter - def logy(self, value: bool): - self.yscale = 'log' if value else 'linear' - - def reset_mode(self): - """ - Reset the modebar mode to nothing, to disable all zoom/pan tools. - """ - self.fig.update_layout(dragmode=False) - - def zoom(self): - """ - Activate the underlying Plotly zoom tool. - """ - self.fig.update_layout(dragmode='zoom') - - def pan(self): - """ - Activate the underlying Plotly pan tool. - """ - self.fig.update_layout(dragmode='pan') - - def panzoom(self, value: Literal['pan', 'zoom'] | None): - """ - Activate or deactivate the pan or zoom tool, depending on the input value. - """ - if value == 'zoom': - self.zoom() - elif value == 'pan': - self.pan() - elif value is None: - self.reset_mode() - - def download_figure(self): - """ - Save the figure to a PNG file via a pop-up dialog. - """ - self.fig.write_image('figure.png') - - def toggle_logx(self): - """ - Toggle the scale between ``linear`` and ``log`` along the horizontal axis. - """ - self.xscale = 'log' if self.xscale in ('linear', None) else 'linear' - - def toggle_logy(self): - """ - Toggle the scale between ``linear`` and ``log`` along the vertical axis. - """ - self.yscale = 'log' if self.yscale in ('linear', None) else 'linear' - - def draw(self): - pass - - def update_legend(self): - pass - - def has_user_xlabel(self) -> bool: - """ - Return ``True`` if the user has set an x-axis label. - """ - return self._xlabel is not None - - def has_user_ylabel(self) -> bool: - """ - Return ``True`` if the user has set a y-axis label. - """ - return self._ylabel is not None diff --git a/src/plopp/backends/plotly/figure.py b/src/plopp/backends/plotly/figure.py deleted file mode 100644 index 30cddf6f8..000000000 --- a/src/plopp/backends/plotly/figure.py +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) - -from ipywidgets import HBox, VBox - -from ...graphics import BaseFig -from ...widgets import HBar, VBar, make_toolbar_canvas2d - - -class Figure(BaseFig, VBox): - """ - Create an interactive figure to represent one-dimensional data. - """ - - def __init__(self, View, *args, **kwargs): - self.interactive = True - self.view = View(*args, **kwargs) - self.toolbar = make_toolbar_canvas2d(view=self.view) - self.left_bar = VBar([self.toolbar]) - self.right_bar = VBar() - self.bottom_bar = HBar() - self.top_bar = HBar() - - super().__init__( - [ - self.top_bar, - HBox([self.left_bar, self.view.canvas.to_widget(), self.right_bar]), - self.bottom_bar, - ] - ) - - def save(self, filename, **kwargs): - """ - Save the figure to file. - The default directory for writing the file is the same as the - directory where the script or notebook is running. - - Parameters - ---------- - filename: - Name of the output file. Possible file extensions are ``.jpg``, ``.png``, - ``.svg``, ``.pdf``, and ``html``. - """ - return self.view.canvas.save(filename, **kwargs) diff --git a/src/plopp/backends/plotly/line.py b/src/plopp/backends/plotly/line.py deleted file mode 100644 index b670fcc14..000000000 --- a/src/plopp/backends/plotly/line.py +++ /dev/null @@ -1,319 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) - -import uuid -from typing import Literal - -import numpy as np -import plotly.graph_objects as go -import scipp as sc -from plotly.colors import qualitative as plotly_colors - -from ...graphics.bbox import BoundingBox -from ..common import check_ndim, make_line_bbox, make_line_data -from .canvas import Canvas - - -def _parse_dicts_in_kwargs(kwargs, name): - out = {} - for key, value in kwargs.items(): - if isinstance(value, dict): - if name in value: - out[key] = value[name] - else: - out[key] = value - return out - - -class Line: - """ - Artist to represent one-dimensional data. - If the coordinate is bin centers, the line is (by default) a set of markers. - If the coordinate is bin edges, the line is a step function. - - Parameters - ---------- - canvas: - The canvas that will display the line. - data: - The initial data to create the line from. - uid: - The unique identifier of the artist. If None, a random UUID is generated. - artist_number: - The canvas keeps track of how many lines have been added to it. This number is - used to set the color and marker parameters of the line. - errorbars: - Show errorbars if ``True``. - mask_color: - The color to be used to represent the masks. - mode: - The mode of the line, either 'markers' or 'lines'. - marker: - The marker style to use. - """ - - def __init__( - self, - canvas: Canvas, - data: sc.DataArray, - uid: str | None = None, - artist_number: int = 0, - errorbars: bool = True, - mask_color: str = 'black', - mode: str | None = None, - marker: str | None = None, - **kwargs, - ): - check_ndim(data, ndim=1, origin='Line') - self.uid = uid if uid is not None else uuid.uuid4().hex - self._fig = canvas.fig - self._data = data - - line_args = _parse_dicts_in_kwargs(kwargs, name=data.name) - - self._line = None - self._mask = None - self._error = None - self._unit = None - self.label = data.name - self._dim = self._data.dim - self._unit = self._data.unit - self._coord = self._data.coords[self._dim] - - line_data = make_line_data(data=self._data, dim=self._dim) - - default_colors = plotly_colors.Plotly - default_line_style = { - 'color': default_colors[artist_number % len(default_colors)] - } - default_marker_style = { - 'symbol': artist_number % 52 # Plotly has 52 marker styles - } - - line_shape = None - - if mode is None: - if line_data["hist"]: - line_shape = 'vh' - mode = 'lines' - else: - mode = 'markers' - - marker_style = default_marker_style if marker is None else marker - line_style = {**default_line_style, **line_args} - - self._line = go.Scatter( - x=np.asarray(line_data['values']['x']), - y=np.asarray(line_data['values']['y']), - name=self.label, - mode=mode, - marker=marker_style, - line_shape=line_shape, - line=line_style, - ) - - if errorbars and (line_data['stddevs'] is not None): - self._error = go.Scatter( - x=np.asarray(line_data['stddevs']['x']), - y=np.asarray(line_data['stddevs']['y']), - line=line_style, - name=self.label, - mode='markers', - marker={'opacity': 0}, - error_y={'type': 'data', 'array': line_data['stddevs']['e']}, - showlegend=False, - ) - - marker_line_style = {'width': 3, 'color': mask_color} - if 'line' in marker_style: - marker_style['line'].update(marker_line_style) - else: - marker_style['line'] = marker_line_style - if 'width' in line_style: - line_style['width'] *= 5 - else: - line_style['width'] = 5 - if 'lines' in mode: - line_style['color'] = mask_color - - self._mask = go.Scatter( - x=np.asarray(line_data['mask']['x']), - y=np.asarray(line_data['mask']['y']), - name=self.label, - mode=mode, - marker=marker_style, - line_shape=line_shape, - line=line_style, - visible=line_data['mask']['visible'], - showlegend=False, - ) - - # Below, we need to re-define the line because it seems that the Scatter trace - # that ends up in the figure is a copy of the one above. - # Plotly has no concept of zorder, so we need to add the traces in a specific - # order - if 'lines' in mode: - self._fig.add_trace(self._mask) - self._mask = self._fig.data[-1] - self._fig.add_trace(self._line) - self._line = self._fig.data[-1] - if self._error is not None: - self._fig.add_trace(self._error) - self._error = self._fig.data[-1] - else: - self._fig.add_trace(self._line) - self._line = self._fig.data[-1] - if self._error is not None: - self._fig.add_trace(self._error) - self._error = self._fig.data[-1] - self._fig.add_trace(self._mask) - self._mask = self._fig.data[-1] - - self._line._plopp_id = self.uid - self._mask._plopp_id = self.uid - if self._error is not None: - self._error._plopp_id = self.uid - - def update(self, new_values: sc.DataArray): - """ - Update the x and y positions of the data points from new data. - - Parameters - ---------- - new_values: - New data to update the line values, masks, errorbars from. - """ - check_ndim(new_values, ndim=1, origin='Line') - self._data = new_values - line_data = make_line_data(data=self._data, dim=self._dim) - - with self._fig.batch_update(): - self._line.update( - {'x': line_data['values']['x'], 'y': line_data['values']['y']} - ) - - if (self._error is not None) and (line_data['stddevs'] is not None): - self._error.update( - { - 'x': line_data['stddevs']['x'], - 'y': line_data['stddevs']['y'], - 'error_y': {'array': line_data['stddevs']['e']}, - } - ) - - if line_data['mask']['visible']: - update = {'x': line_data['mask']['x'], 'y': line_data['mask']['y']} - self._mask.update(update) - self._mask.visible = True - else: - self._mask.visible = False - - def remove(self): - """ - Remove the line, masks and errorbar artists from the canvas. - """ - self._fig.data = [ - trace for trace in list(self._fig.data) if trace._plopp_id != self.uid - ] - - @property - def color(self) -> str: - """ - The line color. - """ - return self._line.line.color - - @color.setter - def color(self, val: str): - self._line.line.color = val - - @property - def style(self) -> str: - """ - The line style. - """ - return self._line.mode - - @style.setter - def style(self, val: str): - self._line.mode = val - - @property - def width(self) -> float: - """ - The line width. - """ - return self._line.line.width - - @width.setter - def width(self, val: float): - self._line.line.width = val - - @property - def marker(self) -> str: - """ - The marker style. - """ - return self._line.marker - - @marker.setter - def marker(self, val: str): - self._line.marker = val - self._mask.marker = val - - @property - def visible(self) -> bool: - """ - The visibility of the line. - """ - return self._line.visible - - @visible.setter - def visible(self, val: bool): - self._line.visible = val - self._mask.visible = val - if self._error is not None: - self._error.visible = val - - @property - def opacity(self) -> float: - """ - The opacity of the line. - """ - return self._line.opacity - - @opacity.setter - def opacity(self, val: float): - self._line.opacity = val - self._mask.opacity = val - if self._error is not None: - self._error.opacity = val - - def bbox( - self, xscale: Literal['linear', 'log'], yscale: Literal['linear', 'log'] - ) -> BoundingBox: - """ - The bounding box of the line. - This includes the x and y bounds of the line and optionally the error bars. - - Parameters - ---------- - xscale: - The scale of the x-axis. - yscale: - The scale of the y-axis. - """ - out = make_line_bbox( - data=self._data, - dim=self._dim, - errorbars=self._error is not None, - xscale=xscale, - yscale=yscale, - ) - if xscale == 'log': - out.xmin = np.log10(out.xmin) - out.xmax = np.log10(out.xmax) - if yscale == 'log': - out.ymin = np.log10(out.ymin) - out.ymax = np.log10(out.ymax) - return out diff --git a/tests/backends/plotly/conftest.py b/tests/backends/plotly/conftest.py deleted file mode 100644 index 2e5e092fc..000000000 --- a/tests/backends/plotly/conftest.py +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) - -import pytest - -import plopp as pp - - -@pytest.fixture(autouse=True, scope='module') -def _use_plotly(): - pp.backends['2d'] = 'plotly' - yield - pp.backends.reset() diff --git a/tests/backends/plotly/plotly_line_test.py b/tests/backends/plotly/plotly_line_test.py deleted file mode 100644 index 5dc87d56e..000000000 --- a/tests/backends/plotly/plotly_line_test.py +++ /dev/null @@ -1,114 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) - -import numpy as np -import pytest -import scipp as sc - -pytest.importorskip("plotly") - - -from plopp.backends.plotly.canvas import Canvas -from plopp.backends.plotly.line import Line -from plopp.data.testing import data_array - - -def test_line_creation(): - da = data_array(ndim=1, unit='K') - line = Line(canvas=Canvas(), data=da) - assert line._unit == 'K' - assert line._dim == 'xx' - assert len(line._line.x) == da.sizes['xx'] - assert np.allclose(line._line.x, da.coords['xx'].values) - assert np.allclose(line._line.y, da.values) - assert line._error is None - assert not line._mask.visible - - -def test_line_creation_bin_edges(): - da = data_array(ndim=1, binedges=True) - line = Line(canvas=Canvas(), data=da) - assert len(line._line.x) == da.sizes['xx'] + 1 - - -def test_line_with_errorbars(): - da = data_array(ndim=1, variances=True) - line = Line(canvas=Canvas(), data=da) - assert np.allclose(line._error.error_y['array'], sc.stddevs(da.data).values) - - -def test_line_with_bin_edges_and_errorbars(): - da = data_array(ndim=1, binedges=True, variances=True) - line = Line(canvas=Canvas(), data=da) - assert np.allclose(line._error.x, sc.midpoints(da.coords['xx']).values) - - -def test_line_hide_errorbars(): - da = data_array(ndim=1, variances=True) - line = Line(canvas=Canvas(), data=da, errorbars=False) - assert line._error is None - - -def test_line_with_mask(): - da = data_array(ndim=1, masks=True) - line = Line(canvas=Canvas(), data=da) - assert line._mask.visible - - -def test_line_with_mask_and_binedges(): - da = data_array(ndim=1, binedges=True, masks=True) - line = Line(canvas=Canvas(), data=da) - assert line._mask.visible - - -def test_line_with_two_masks(): - da = data_array(ndim=1, masks=True) - da.masks['two'] = da.coords['xx'] > sc.scalar(25, unit='m') - line = Line(canvas=Canvas(), data=da) - expected = da.data[da.masks['mask'] | da.masks['two']].values - y = line._mask.y - assert np.allclose(y[~np.isnan(y)], expected) - - -def test_line_update(): - da = data_array(ndim=1) - line = Line(canvas=Canvas(), data=da) - assert np.allclose(line._line.x, da.coords['xx'].values) - assert np.allclose(line._line.y, da.values) - line.update(da * 2.5) - assert np.allclose(line._line.x, da.coords['xx'].values) - assert np.allclose(line._line.y, da.values * 2.5) - - -def test_line_update_with_errorbars(): - da = data_array(ndim=1, variances=True) - line = Line(canvas=Canvas(), data=da) - assert np.allclose(line._line.y, da.values) - assert np.allclose(line._error.error_y['array'], sc.stddevs(da.data).values) - new_values = da * 3.3 - new_values.variances = da.variances - line.update(new_values) - assert np.allclose(line._line.y, da.values * 3.3) - assert np.allclose(line._error.error_y['array'], sc.stddevs(da.data).values) - new_values = 1.0 * da - new_values.variances = da.variances * 4.0 - line.update(new_values) - assert np.allclose(line._line.y, da.values) - assert np.allclose(line._error.error_y['array'], sc.stddevs(da.data).values * 2.0) - - -def test_line_datetime_binedges_with_errorbars(): - t = np.arange( - np.datetime64('2017-03-16T20:58:17'), np.datetime64('2017-03-16T21:15:17'), 20 - ) - time = sc.array(dims=['time'], values=t) - v = np.random.rand(time.sizes['time'] - 1) - da = sc.DataArray( - data=sc.array(dims=['time'], values=10 * v, variances=v), coords={'time': time} - ) - xint = t.astype(int) - xmid = (0.5 * (xint[1:] + xint[:-1])).astype(int) - expected = np.array(xmid, dtype=t.dtype) - line = Line(canvas=Canvas(), data=da) - # Note that allclose does not work on datetime dtypes - assert np.allclose(line._error.x.astype(int), expected.astype(int)) diff --git a/tests/conftest.py b/tests/conftest.py index 68377fb1c..662c2ac6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,7 +46,6 @@ def pytest_sessionfinish(session, exitstatus): BACKENDS_MPL = [('2d', 'mpl-static'), ('2d', 'mpl-interactive')] BACKENDS_MPL_INTERACTIVE = [('2d', 'mpl-interactive')] -BACKENDS_PLOTLY = [('2d', 'plotly')] if util.find_spec('plotly') is not None else [] def _select_backend(backend): @@ -75,12 +74,12 @@ def _parametrize_mpl_backends(request): _select_backend(request.param) -@pytest.fixture(**_make_fixture_args(BACKENDS_MPL + BACKENDS_PLOTLY)) +@pytest.fixture(**_make_fixture_args(BACKENDS_MPL)) def _parametrize_all_backends(request): _select_backend(request.param) -@pytest.fixture(**_make_fixture_args(BACKENDS_MPL_INTERACTIVE + BACKENDS_PLOTLY)) +@pytest.fixture(**_make_fixture_args(BACKENDS_MPL_INTERACTIVE)) def _parametrize_interactive_1d_backends(request): _select_backend(request.param) diff --git a/tests/graphics/artists_test.py b/tests/graphics/artists_test.py index 3a622ba44..88d0c2f96 100644 --- a/tests/graphics/artists_test.py +++ b/tests/graphics/artists_test.py @@ -69,23 +69,6 @@ ), } -if util.find_spec('plotly') is not None: - CASES.update( - { - "line-plotly": (('2d', 'plotly'), pp.plot, {'obj': pp.data.data1d()}), - "line-plotly-masks": ( - ('2d', 'plotly'), - pp.plot, - {'obj': pp.data.data1d(masks=True)}, - ), - "line-plotly-errorbars": ( - ('2d', 'plotly'), - pp.plot, - {'obj': pp.data.data1d(variances=True)}, - ), - } - ) - @pytest.mark.parametrize(("backend", "func", "data"), CASES.values(), ids=CASES.keys()) class TestArtists: diff --git a/tests/graphics/canvas_test.py b/tests/graphics/canvas_test.py index 882a3bb15..e37deb26a 100644 --- a/tests/graphics/canvas_test.py +++ b/tests/graphics/canvas_test.py @@ -40,13 +40,6 @@ ), } -if util.find_spec('plotly') is not None: - CASES.update( - { - "linefigure-plotly": (('2d', 'plotly'), linefigure, data1d), - } - ) - @pytest.mark.parametrize( ("backend", "figure", "data"), CASES.values(), ids=CASES.keys() diff --git a/tests/graphics/figures_test.py b/tests/graphics/figures_test.py index dd50370dc..fb278dc25 100644 --- a/tests/graphics/figures_test.py +++ b/tests/graphics/figures_test.py @@ -26,13 +26,6 @@ ), } -if util.find_spec('plotly') is not None: - PLOTCASES.update( - { - "linefigure-plotly": (('2d', 'plotly'), linefigure, data1d), - } - ) - SCATTERCASES = { "scatterfigure-mpl-static": ( diff --git a/tests/plotting/plot_1d_test.py b/tests/plotting/plot_1d_test.py index 633c4b0c0..486f70f58 100644 --- a/tests/plotting/plot_1d_test.py +++ b/tests/plotting/plot_1d_test.py @@ -14,16 +14,6 @@ pytestmark = pytest.mark.usefixtures("_parametrize_all_backends") -def _skip_if_kaleido_not_installed(): - if pp.backends['2d'] == 'plotly': - try: - import kaleido as kldo - except ImportError: - kldo = None - if kldo is None: - pytest.skip("Skipping because kaleido is not installed") - - def test_plot_ndarray(): pp.plot(np.arange(50.0)) @@ -195,7 +185,6 @@ def test_use_non_dimension_coords_dataset(): @pytest.mark.parametrize('ext', ['jpg', 'png', 'pdf', 'svg']) def test_save_to_disk_1d(ext): - _skip_if_kaleido_not_installed() da = data_array(ndim=1) fig = pp.plot(da) with tempfile.TemporaryDirectory() as path: @@ -205,7 +194,6 @@ def test_save_to_disk_1d(ext): def test_save_to_disk_with_bad_extension_raises(): - _skip_if_kaleido_not_installed() da = data_array(ndim=1) fig = pp.plot(da) with pytest.raises(ValueError, match='txt'): @@ -356,8 +344,6 @@ def test_plot_1d_datetime_coord_with_mask_and_binedges(): def test_plot_1d_datetime_coord_log(): - if pp.backends['2d'] == 'plotly': - pytest.skip('Log scale with datetime not supported in plotly') t = np.arange( np.datetime64('2017-03-16T20:58:17'), np.datetime64('2017-03-16T21:15:17'), 20 ) @@ -371,8 +357,6 @@ def test_plot_1d_datetime_coord_log(): def test_plot_1d_datetime_coord_log_binedges(): - if pp.backends['2d'] == 'plotly': - pytest.skip('Log scale with datetime not supported in plotly') t = np.arange( np.datetime64('2017-03-16T20:58:17'), np.datetime64('2017-03-16T21:15:17'), 20 ) @@ -386,8 +370,6 @@ def test_plot_1d_datetime_coord_log_binedges(): def test_plot_1d_datetime_coord_log_with_mask(): - if pp.backends['2d'] == 'plotly': - pytest.skip('Log scale with datetime not supported in plotly') t = np.arange( np.datetime64('2017-03-16T20:58:17'), np.datetime64('2017-03-16T21:15:17'), 20 ) From cb83fa9c9755b49f1b88cffc61ded1c73a0a4055 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 10 Jun 2026 16:53:21 +0200 Subject: [PATCH 2/4] also remove from tox and ci --- .github/workflows/ci.yml | 2 +- .github/workflows/weekly_windows_macos.yml | 2 +- tox.ini | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f5d822b5..bd9b5a800 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: matrix: os: ['ubuntu-24.04'] python: ['${{needs.formatting.outputs.min_python}}'] - tox-env: ['${{needs.formatting.outputs.min_tox_env}}', 'package-test', 'minimal', 'noplotly'] + tox-env: ['${{needs.formatting.outputs.min_tox_env}}', 'package-test', 'minimal'] uses: ./.github/workflows/test.yml with: os-variant: ${{ matrix.os }} diff --git a/.github/workflows/weekly_windows_macos.yml b/.github/workflows/weekly_windows_macos.yml index c9983b5b9..0dfb2442c 100644 --- a/.github/workflows/weekly_windows_macos.yml +++ b/.github/workflows/weekly_windows_macos.yml @@ -26,7 +26,7 @@ jobs: matrix: os: ['macos-latest', 'windows-latest'] python: ['${{needs.pytox.outputs.min_python}}'] - tox-env: ['${{needs.pytox.outputs.min_tox_env}}', 'package-test', 'minimal', 'noplotly'] + tox-env: ['${{needs.pytox.outputs.min_tox_env}}', 'package-test', 'minimal'] uses: ./.github/workflows/test.yml with: os-variant: ${{ matrix.os }} diff --git a/tox.ini b/tox.ini index 7c279e065..909ec1a03 100644 --- a/tox.ini +++ b/tox.ini @@ -15,11 +15,6 @@ deps = pytest commands = pytest tests/minimal_plot_test.py -[testenv:noplotly] -description = Test that plotly tests are skipped if plotly is not installed -deps = -r requirements/noplotly.txt -commands = pytest tests - [testenv:package-test] description = Test the package can be imported deps = From 790f66c7d44061c6dd715575071884a0730181ed Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 10 Jun 2026 17:19:36 +0200 Subject: [PATCH 3/4] remove unused imports --- tests/conftest.py | 2 -- tests/graphics/artists_test.py | 1 - tests/graphics/canvas_test.py | 1 - tests/graphics/figures_test.py | 1 - 4 files changed, 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 662c2ac6b..8b5e88b3d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,6 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) -from importlib import util - import matplotlib import matplotlib.pyplot as plt import pytest diff --git a/tests/graphics/artists_test.py b/tests/graphics/artists_test.py index 88d0c2f96..b2641b0a6 100644 --- a/tests/graphics/artists_test.py +++ b/tests/graphics/artists_test.py @@ -2,7 +2,6 @@ # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) from functools import partial -from importlib import util import pytest diff --git a/tests/graphics/canvas_test.py b/tests/graphics/canvas_test.py index e37deb26a..17d7cd94f 100644 --- a/tests/graphics/canvas_test.py +++ b/tests/graphics/canvas_test.py @@ -2,7 +2,6 @@ # Copyright (c) 2024 Scipp contributors (https://github.com/scipp) from functools import partial -from importlib import util import pytest diff --git a/tests/graphics/figures_test.py b/tests/graphics/figures_test.py index fb278dc25..f951241ee 100644 --- a/tests/graphics/figures_test.py +++ b/tests/graphics/figures_test.py @@ -2,7 +2,6 @@ # Copyright (c) 2024 Scipp contributors (https://github.com/scipp) from functools import partial -from importlib import util import pytest import scipp as sc From a42b54e3a0a8d6f5a3996e704e92c6936b4d8302 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 10 Jun 2026 21:27:03 +0200 Subject: [PATCH 4/4] remove plotly from api docs --- docs/api-reference/index.md | 1 - docs/api-reference/plotly.md | 12 ------------ 2 files changed, 13 deletions(-) delete mode 100644 docs/api-reference/plotly.md diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index 40cf57466..c43bb2c2e 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -92,6 +92,5 @@ :maxdepth: 1 matplotlib - plotly pythreejs ``` diff --git a/docs/api-reference/plotly.md b/docs/api-reference/plotly.md deleted file mode 100644 index 76386c6be..000000000 --- a/docs/api-reference/plotly.md +++ /dev/null @@ -1,12 +0,0 @@ -# Plotly backend - -```{eval-rst} -.. currentmodule:: plopp - -.. autosummary:: - :toctree: ../generated - - backends.plotly.canvas.Canvas - backends.plotly.figure.Figure - backends.plotly.line.Line -```