Browse Source

Improve interaction with Matplotlib (#2553)

* WIP: improve interaction with Matplotlib

This is a sketch of how to improve the interaction with Matplotlib.

The first idea is to directly create `Figure` objects rather than relying on
pyplot.  This de-couples the UI from the global state that is the pyplot figure
registry and ensures that you never get surprise GUI windows popping up (or a
memory leak) when pyplot selects a GUI backend.

The second suggestion is to have the object return by the context manager be
able to create multiple figures (not actually implemented).

The third is to support saving the figures as png instead of svg (not actually
implemented).

* code review and simplification

* fix matplotlib figure context

* fix type annotation

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Thomas A Caswell 1 year ago
parent
commit
3be318a579

+ 43 - 0
nicegui/elements/pyplot.py

@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import asyncio
 import io
 import os
@@ -11,8 +13,22 @@ from ..element import Element
 
 try:
     if os.environ.get('MATPLOTLIB', 'true').lower() == 'true':
+        import matplotlib.figure
         import matplotlib.pyplot as plt
         optional_features.register('matplotlib')
+
+        class MatplotlibFigure(matplotlib.figure.Figure):
+
+            def __init__(self, element: Matplotlib, *args: Any, **kwargs: Any) -> None:
+                super().__init__(*args, **kwargs)
+                self.element = element
+
+            def __enter__(self) -> Self:
+                return self
+
+            def __exit__(self, *_) -> None:
+                self.element.update()
+
 except ImportError:
     pass
 
@@ -57,3 +73,30 @@ class Pyplot(Element):
         while self.client.id in Client.instances:
             await asyncio.sleep(1.0)
         plt.close(self.fig)
+
+
+class Matplotlib(Element):
+
+    def __init__(self, **kwargs: Any) -> None:
+        """Matplotlib
+
+        Create a `Matplotlib <https://matplotlib.org/>`_ element rendering a Matplotlib figure.
+        The figure is automatically updated when leaving the figure context.
+
+        :param kwargs: arguments like `figsize` which should be passed to `matplotlib.figure.Figure <https://matplotlib.org/stable/api/figure_api.html#matplotlib.figure.Figure>`_
+        """
+        if not optional_features.has('matplotlib'):
+            raise ImportError('Matplotlib is not installed. Please run "pip install matplotlib".')
+
+        super().__init__('div')
+        self.figure = MatplotlibFigure(self, **kwargs)
+        self._convert_to_html()
+
+    def _convert_to_html(self) -> None:
+        with io.StringIO() as output:
+            self.figure.savefig(output, format='svg')
+            self._props['innerHTML'] = output.getvalue()
+
+    def update(self) -> None:
+        self._convert_to_html()
+        return super().update()

+ 2 - 0
nicegui/ui.py

@@ -50,6 +50,7 @@ __all__ = [
     'list',
     'log',
     'markdown',
+    'matplotlib',
     'menu',
     'menu_item',
     'mermaid',
@@ -179,6 +180,7 @@ from .elements.pagination import Pagination as pagination
 from .elements.plotly import Plotly as plotly
 from .elements.progress import CircularProgress as circular_progress
 from .elements.progress import LinearProgress as linear_progress
+from .elements.pyplot import Matplotlib as matplotlib
 from .elements.pyplot import Pyplot as pyplot
 from .elements.query import Query as query
 from .elements.radio import Radio as radio

+ 17 - 0
website/documentation/content/matplotlib_documentation.py

@@ -0,0 +1,17 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.matplotlib)
+def main_demo() -> None:
+    import numpy as np
+
+    with ui.matplotlib(figsize=(3, 2)).figure as fig:
+        x = np.linspace(0.0, 5.0)
+        y = np.cos(2 * np.pi * x) * np.exp(-x)
+        ax = fig.gca()
+        ax.plot(x, y, '-')
+
+
+doc.reference(ui.matplotlib)

+ 4 - 3
website/documentation/content/section_data_elements.py

@@ -2,9 +2,9 @@ from nicegui import optional_features
 
 from . import (aggrid_documentation, circular_progress_documentation, code_documentation, doc, echart_documentation,
                editor_documentation, highchart_documentation, json_editor_documentation, leaflet_documentation,
-               line_plot_documentation, linear_progress_documentation, log_documentation, plotly_documentation,
-               pyplot_documentation, scene_documentation, spinner_documentation, table_documentation,
-               tree_documentation)
+               line_plot_documentation, linear_progress_documentation, log_documentation, matplotlib_documentation,
+               plotly_documentation, pyplot_documentation, scene_documentation, spinner_documentation,
+               table_documentation, tree_documentation)
 
 doc.title('*Data* Elements')
 
@@ -15,6 +15,7 @@ if optional_features.has('highcharts'):
 doc.intro(echart_documentation)
 if optional_features.has('matplotlib'):
     doc.intro(pyplot_documentation)
+    doc.intro(matplotlib_documentation)
     doc.intro(line_plot_documentation)
 if optional_features.has('plotly'):
     doc.intro(plotly_documentation)