diff --git a/.copier-answers.yml b/.copier-answers.yml index 87f96d4..185c203 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,7 +1,7 @@ # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY _commit: 47497b5 _src_path: gh:scipp/copier_template -description: Matplotlib replacement for Jupyter that uses WebGL via Pythreejs +description: Matplotlib clone for Jupyter that uses WebGL via Pythreejs max_python: '3.13' min_python: '3.11' namespace_package: '' diff --git a/README.md b/README.md index 8d6989b..24a6ab4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## About -Matplotlib replacement for Jupyter that uses WebGL via Pythreejs +Matplotlib clone for Jupyter that uses WebGL via Pythreejs ## Installation diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000..31c6f22 Binary files /dev/null and b/docs/_static/favicon.ico differ diff --git a/docs/_static/logo-dark.svg b/docs/_static/logo-dark.svg new file mode 100644 index 0000000..3848b20 --- /dev/null +++ b/docs/_static/logo-dark.svg @@ -0,0 +1,70 @@ + + + + diff --git a/docs/_static/logo.svg b/docs/_static/logo.svg new file mode 100644 index 0000000..f7bf6e6 --- /dev/null +++ b/docs/_static/logo.svg @@ -0,0 +1,70 @@ + + + + diff --git a/docs/index.md b/docs/index.md index 208ef8e..cefe54c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ # {transparent}`Matplotgl`
- Matplotlib replacement for Jupyter that uses WebGL via Pythreejs + Matplotlib clone for Jupyter that uses WebGL via Pythreejs

diff --git a/docs/user-guide/imshow.ipynb b/docs/user-guide/imshow.ipynb index 31cfdcb..fe50342 100644 --- a/docs/user-guide/imshow.ipynb +++ b/docs/user-guide/imshow.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "394c0bcc-4177-4083-abbb-9f829ab60a01", + "id": "0", "metadata": {}, "source": [ "# Imshow" @@ -11,7 +11,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7b97da73-c403-48e6-a9a4-d86b8452eca2", + "id": "1", "metadata": {}, "outputs": [], "source": [ @@ -22,7 +22,7 @@ { "cell_type": "code", "execution_count": null, - "id": "650ad4ba-b117-4295-a4d0-55a3df249b1c", + "id": "2", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/user-guide/plot.ipynb b/docs/user-guide/plot.ipynb index 78bdd48..d1d0976 100644 --- a/docs/user-guide/plot.ipynb +++ b/docs/user-guide/plot.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "d228c316-3700-44b4-b457-218454da891d", + "id": "0", "metadata": {}, "source": [ "# Plot" @@ -11,7 +11,7 @@ { "cell_type": "code", "execution_count": null, - "id": "710d60a6-0166-44b5-8460-0e46eff525fa", + "id": "1", "metadata": {}, "outputs": [], "source": [ @@ -22,7 +22,7 @@ { "cell_type": "code", "execution_count": null, - "id": "0bcb7406-4eb8-45af-971e-36c7af397de4", + "id": "2", "metadata": {}, "outputs": [], "source": [ @@ -42,7 +42,7 @@ { "cell_type": "code", "execution_count": null, - "id": "5c0de3de-4ce7-4882-9c0d-db6cb189bce0", + "id": "3", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/user-guide/scatter.ipynb b/docs/user-guide/scatter.ipynb index 76487f8..ec90b57 100644 --- a/docs/user-guide/scatter.ipynb +++ b/docs/user-guide/scatter.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "935f17d3-eae3-41e5-8258-8165cda406bb", + "id": "0", "metadata": {}, "source": [ "# Scatter" @@ -11,7 +11,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9f36776c-0c6b-4d3b-8d76-86961aa11d16", + "id": "1", "metadata": {}, "outputs": [], "source": [ @@ -22,7 +22,7 @@ { "cell_type": "code", "execution_count": null, - "id": "05d54f24-7298-4ec3-befd-85ebaff9d1a6", + "id": "2", "metadata": {}, "outputs": [], "source": [ diff --git a/pyproject.toml b/pyproject.toml index ea8eead..278320d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "matplotgl" -description = "Matplotlib replacement for Jupyter that uses WebGL via Pythreejs" +description = "Matplotlib clone for Jupyter that uses WebGL via Pythreejs" authors = [{ name = "Scipp contributors" }] license = "BSD-3-Clause" license-files = ["LICENSE"] @@ -32,6 +32,7 @@ requires-python = ">=3.11" dependencies = [ "matplotlib", "pythreejs", + "anywidget", ] dynamic = ["version"] @@ -60,7 +61,7 @@ addopts = """ testpaths = "tests" filterwarnings = [ "error", -] + 'ignore:\n Sentinel is not a public part of the traitlets API:DeprecationWarning',] [tool.ruff] line-length = 88 diff --git a/requirements/base.in b/requirements/base.in index a3923cb..f862369 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -4,3 +4,4 @@ # The following was generated by 'tox -e deps', DO NOT EDIT MANUALLY! matplotlib pythreejs +anywidget diff --git a/requirements/base.txt b/requirements/base.txt index 6e74756..09adb19 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,10 +1,12 @@ -# SHA1:04ba1dfd0c00e2f352f80789e53cd5ef22ea5d76 +# SHA1:2c258b7b4a1cf8f84c688edac6f53735d3ea39b8 # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # +anywidget==0.9.18 + # via -r base.in asttokens==3.0.0 # via stack-data comm==0.2.3 @@ -27,6 +29,7 @@ ipython-pygments-lexers==1.1.1 # via ipython ipywidgets==8.1.7 # via + # anywidget # ipydatawidgets # pythreejs jedi==0.19.2 @@ -55,6 +58,8 @@ pillow==12.0.0 # via matplotlib prompt-toolkit==3.0.52 # via ipython +psygnal==0.15.0 + # via anywidget ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 @@ -83,7 +88,9 @@ traitlets==5.14.3 traittypes==0.2.3 # via ipydatawidgets typing-extensions==4.15.0 - # via ipython + # via + # anywidget + # ipython wcwidth==0.2.14 # via prompt-toolkit widgetsnbextension==4.0.14 diff --git a/requirements/make_base.py b/requirements/make_base.py index 4c004af..2cda547 100644 --- a/requirements/make_base.py +++ b/requirements/make_base.py @@ -70,7 +70,9 @@ def as_nightly(repo: str) -> str: nightly = tuple(args.nightly.split(",") if args.nightly else []) -nightly_dependencies = [dep for dep in dependencies + test_dependencies if not dep.startswith(nightly)] +nightly_dependencies = [ + dep for dep in dependencies + test_dependencies if not dep.startswith(nightly) +] nightly_dependencies += [as_nightly(arg) for arg in nightly] write_dependencies("nightly", nightly_dependencies) diff --git a/requirements/nightly.in b/requirements/nightly.in index 7738c80..ba91b3f 100644 --- a/requirements/nightly.in +++ b/requirements/nightly.in @@ -3,4 +3,5 @@ # The following was generated by 'tox -e deps', DO NOT EDIT MANUALLY! matplotlib pythreejs +anywidget pytest diff --git a/requirements/nightly.txt b/requirements/nightly.txt index 8baa616..035afe3 100644 --- a/requirements/nightly.txt +++ b/requirements/nightly.txt @@ -1,10 +1,12 @@ -# SHA1:bd1b1a61cb415a83b0c4c4c36380945b04796f84 +# SHA1:d2cf1c994df2f0f7d49dbb832580621379e9d486 # # This file was generated by pip-compile-multi. # To update, run: # # requirements upgrade # +anywidget==0.9.18 + # via -r nightly.in asttokens==3.0.0 # via stack-data comm==0.2.3 @@ -29,6 +31,7 @@ ipython-pygments-lexers==1.1.1 # via ipython ipywidgets==8.1.7 # via + # anywidget # ipydatawidgets # pythreejs jedi==0.19.2 @@ -61,6 +64,8 @@ pluggy==1.6.0 # via pytest prompt-toolkit==3.0.52 # via ipython +psygnal==0.15.0 + # via anywidget ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 @@ -92,7 +97,9 @@ traitlets==5.14.3 traittypes==0.2.3 # via ipydatawidgets typing-extensions==4.15.0 - # via ipython + # via + # anywidget + # ipython wcwidth==0.2.14 # via prompt-toolkit widgetsnbextension==4.0.14 diff --git a/src/matplotgl/axes.py b/src/matplotgl/axes.py index e689448..6faf194 100644 --- a/src/matplotgl/axes.py +++ b/src/matplotgl/axes.py @@ -2,15 +2,14 @@ # Copyright (c) 2023 Matplotgl contributors (https://github.com/matplotgl) import ipywidgets as ipw +import numpy as np import pythreejs as p3 from matplotlib.axes import Axes as MplAxes -import numpy as np - -from .line import Line -from .points import Points from .image import Image +from .line import Line from .mesh import Mesh +from .points import Points from .utils import latex_to_html from .widgets import ClickableHTML @@ -23,6 +22,7 @@ def __init__(self, *, ax: MplAxes, figure=None) -> None: self._ymin = 0.0 self._ymax = 1.0 self._fig = None + self._spine_linewidth = 1.0 self._ax = ax self._artists = [] self.lines = [] @@ -136,7 +136,12 @@ def __init__(self, *, ax: MplAxes, figure=None) -> None: ) self._margins["cursor"] = ipw.Label( "(0.00, 0.00)", - layout={"grid_area": "cursor", "padding": "0", "margin": "0"}, + layout={ + "grid_area": "cursor", + "padding": "0", + "margin": "0", + "width": "80px", + }, ) if figure is not None: @@ -337,13 +342,14 @@ def _make_xticks(self): bottom_string = ( f'' + f'style="stroke:black;stroke-width:{self._spine_linewidth}" />' ) self._margins["topspine"].value = ( f'' f'' + f'y2="{self._thin_margin}" style="stroke:black;stroke-width:' + f'{self._spine_linewidth}" />' ) for tick, label in zip(xticks_axes, xlabels, strict=True): @@ -390,13 +396,13 @@ def _make_yticks(self): f'' f'' + f'style="stroke:black;stroke-width:{self._spine_linewidth}" />' ) self._margins["rightspine"].value = ( f'' f'' + f'style="stroke:black;stroke-width:{self._spine_linewidth}" />' ) for tick, label in zip(yticks_axes, ytexts, strict=True): diff --git a/src/matplotgl/figure.py b/src/matplotgl/figure.py index 842acd5..e9d9107 100644 --- a/src/matplotgl/figure.py +++ b/src/matplotgl/figure.py @@ -1,11 +1,10 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Matplotgl contributors (https://github.com/matplotgl) +from .colorbar import Colorbar from .toolbar import Toolbar from .widgets import HBar -from .colorbar import Colorbar - class Figure(HBar): def __init__(self, *, figsize=(5.0, 3.5), dpi=96, nrows=1, ncols=1) -> None: @@ -35,7 +34,8 @@ def toggle_zoom(self, change): ax._zoom_down_picker.observe(ax.on_mouse_down, names=["point"]) ax._zoom_up_picker.observe(ax.on_mouse_up, names=["point"]) ax._zoom_move_picker.observe(ax.on_mouse_move, names=["point"]) - ax.renderer.controls = ax._base_controls + [ + ax.renderer.controls = [ + *ax._base_controls, ax._zoom_down_picker, ax._zoom_up_picker, ax._zoom_move_picker, diff --git a/src/matplotgl/image.py b/src/matplotgl/image.py index 7af8691..c9749a8 100644 --- a/src/matplotgl/image.py +++ b/src/matplotgl/image.py @@ -1,10 +1,9 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Matplotgl contributors (https://github.com/matplotgl) -import pythreejs as p3 import matplotlib as mpl import numpy as np - +import pythreejs as p3 from .norm import Normalizer @@ -13,7 +12,7 @@ class Image: def __init__( self, array: np.ndarray, - extent: list[float] = None, + extent: list[float] | None = None, cmap: str = "viridis", zorder: float = 0, ): diff --git a/src/matplotgl/line.py b/src/matplotgl/line.py index 289b438..a75036c 100644 --- a/src/matplotgl/line.py +++ b/src/matplotgl/line.py @@ -2,11 +2,12 @@ # Copyright (c) 2023 Matplotgl contributors (https://github.com/matplotgl) import warnings -from matplotlib import colors as mplc -import pythreejs as p3 + import numpy as np +import pythreejs as p3 +from matplotlib import colors as mplc -from .utils import fix_empty_range, find_limits +from .utils import find_limits, fix_empty_range class Line: diff --git a/src/matplotgl/mesh.py b/src/matplotgl/mesh.py index ae5c2a9..d730007 100644 --- a/src/matplotgl/mesh.py +++ b/src/matplotgl/mesh.py @@ -2,11 +2,12 @@ # Copyright (c) 2023 Matplotgl contributors (https://github.com/matplotgl) import warnings -import pythreejs as p3 + import matplotlib as mpl import numpy as np +import pythreejs as p3 -from .utils import fix_empty_range, find_limits +from .utils import find_limits, fix_empty_range class Mesh: diff --git a/src/matplotgl/points.py b/src/matplotgl/points.py index 4561d21..ac47ce3 100644 --- a/src/matplotgl/points.py +++ b/src/matplotgl/points.py @@ -2,14 +2,13 @@ # Copyright (c) 2023 Matplotgl contributors (https://github.com/matplotgl) import warnings -import numpy as np -import pythreejs as p3 -import matplotlib.colors as mplc import matplotlib as mpl +import matplotlib.colors as mplc +import numpy as np +import pythreejs as p3 -from .utils import fix_empty_range, find_limits - +from .utils import find_limits, fix_empty_range SHADER_LIBRARY = { "o": """ diff --git a/src/matplotgl/pyplot.py b/src/matplotgl/pyplot.py index df4c03e..5b89441 100644 --- a/src/matplotgl/pyplot.py +++ b/src/matplotgl/pyplot.py @@ -1,15 +1,13 @@ # SPDX-License-Identifier: BSD-3-Clause import matplotlib -from matplotlib.figure import Figure as MplFigure - import numpy as np +from matplotlib.figure import Figure as MplFigure from .axes import Axes from .figure import Figure from .widgets import VBar - matplotlib.use("Agg") # Headless backend diff --git a/src/matplotgl/subplots.py b/src/matplotgl/subplots.py index 490cebe..7fb2daf 100644 --- a/src/matplotgl/subplots.py +++ b/src/matplotgl/subplots.py @@ -1,9 +1,8 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Matplotgl contributors (https://github.com/matplotgl) -import numpy as np - import matplotlib +import numpy as np from matplotlib.figure import Figure as MplFigure from .axes import Axes diff --git a/src/matplotgl/utils.py b/src/matplotgl/utils.py index 710048a..9079d9b 100644 --- a/src/matplotgl/utils.py +++ b/src/matplotgl/utils.py @@ -1,6 +1,5 @@ -from typing import Literal - import re +from typing import Literal import numpy as np import pythreejs as p3 @@ -24,7 +23,7 @@ def value_to_string(val, precision: int = 3) -> str: elif (abs(val) >= 1.0e4) or (abs(val) <= 1.0e-4): text = "{val:.{prec}e}".format(val=val, prec=precision) else: - text = "{}".format(val) + text = str(val) if len(text) > precision + 2 + (text[0] == "-"): text = "{val:.{prec}f}".format(val=val, prec=precision) return text diff --git a/src/matplotgl/widgets.py b/src/matplotgl/widgets.py index 73ca0c8..57796e3 100644 --- a/src/matplotgl/widgets.py +++ b/src/matplotgl/widgets.py @@ -3,8 +3,7 @@ import anywidget import traitlets - -from ipywidgets import VBox, HBox, Widget +from ipywidgets import HBox, VBox, Widget class Bar: @@ -19,7 +18,7 @@ def add(self, obj: Widget): """ Append a widget to the list of children. """ - self.children = list(self.children) + [obj] + self.children = [*list(self.children), obj] def remove(self, obj: Widget): """ @@ -69,10 +68,9 @@ class Box(VBar): """ def __init__(self, widgets): - children = [] - for view in widgets: - children.append(HBar(view) if isinstance(view, (list, tuple)) else view) - super().__init__(children) + super().__init__( + [HBar(view) if isinstance(view, list | tuple) else view for view in widgets] + ) class ClickableHTML(anywidget.AnyWidget): diff --git a/tests/plot_test.py b/tests/plot_test.py new file mode 100644 index 0000000..7c81261 --- /dev/null +++ b/tests/plot_test.py @@ -0,0 +1,60 @@ +import numpy as np + +import matplotgl.pyplot as plt + + +def test_plot_one_line(): + fig, ax = plt.subplots() + points_per_line = 100 + x = np.linspace(0, 10, points_per_line) + y = np.sin(x) * np.exp(-x / 5) + np.random.uniform(-0.1, 0.1, size=points_per_line) + ax.plot(x, y) + + assert len(fig.axes) == 1 + assert len(ax.lines) == 1 + lines = ax.lines[0] + assert np.allclose(lines.get_xdata(), x) + assert np.allclose(lines.get_ydata(), y) + + +def test_plot_multiple_lines(): + fig, ax = plt.subplots() + points_per_line = 100 + x = np.linspace(0, 10, points_per_line) + y = [] + for i in range(4): + y.append( + np.sin(x + i) * np.exp(-x / 5) + + np.random.uniform(-0.1, 0.1, size=points_per_line) + ) + ax.plot(x, y[i]) + + assert len(fig.axes) == 1 + assert len(ax.lines) == 4 + for i, line in enumerate(ax.lines): + assert np.allclose(line.get_xdata(), x) + assert np.allclose(line.get_ydata(), y[i]) + + +def test_scatter(): + fig, ax = plt.subplots() + x, y = np.random.normal(size=(2, 1000)) + ax.scatter(x, y) + + assert len(fig.axes) == 1 + assert len(ax.collections) == 1 + scatter = ax.collections[0] + assert np.allclose(scatter.get_xdata(), x) + assert np.allclose(scatter.get_ydata(), y) + + +def test_imshow(): + fig, ax = plt.subplots() + data = np.random.rand(200, 300) + ax.imshow(data, cmap='viridis', extent=[0, 10, 0, 5]) + + assert len(fig.axes) == 1 + assert len(ax.images) == 1 + im = ax.images[0] + assert np.allclose(im._array, data) + assert im.get_extent() == [0, 10, 0, 5]