From cb2d4b586973fa135fcef604bf41129d5176ffd4 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 9 Apr 2021 23:06:07 -0400 Subject: [PATCH] Improve Figure.show for displaying previews in Jupyter notebooks and external viewers (#529) Figure.show() now is able to detect the current running environment, and display previews in Jupyter notebooks, or open previews using external viewers. This PR also provides `pygmt.set_display()` to change the display method globally. Setting environmental variable `PYGMT_USE_EXTERNAL_DISPLAY ` to `false` can disable external viewers, mostly for running tests and building the documentations. Figure.show() no longer returns the Image object. Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> Co-authored-by: Meghan Jones --- Makefile | 2 +- doc/Makefile | 3 +- doc/api/index.rst | 7 ++ pygmt/__init__.py | 2 +- pygmt/figure.py | 130 ++++++++++++++++++++++++++----------- pygmt/tests/test_figure.py | 14 +++- 6 files changed, 113 insertions(+), 45 deletions(-) diff --git a/Makefile b/Makefile index e5cae667ecd..a5342aa800d 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ test: @echo "" @cd $(TESTDIR); python -c "import $(PROJECT); $(PROJECT).show_versions()" @echo "" - cd $(TESTDIR); pytest $(PYTEST_COV_ARGS) $(PROJECT) + cd $(TESTDIR); PYGMT_USE_EXTERNAL_DISPLAY="false" pytest $(PYTEST_COV_ARGS) $(PROJECT) cp $(TESTDIR)/coverage.xml . cp -r $(TESTDIR)/htmlcov . rm -r $(TESTDIR) diff --git a/doc/Makefile b/doc/Makefile index 81b3a53fcd2..6c4d7ab31ff 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -31,7 +31,8 @@ html: api @echo @echo "Building HTML files." @echo - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + # Set PYGMT_USE_EXTERNAL_DISPLAY to "false" to disable external display + PYGMT_USE_EXTERNAL_DISPLAY="false" $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." diff --git a/doc/api/index.rst b/doc/api/index.rst index 3b4220b7e7d..6460eb53e28 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -60,6 +60,13 @@ Saving and displaying the figure: Figure.show Figure.psconvert +Configuring the display settings: + +.. autosummary:: + :toctree: generated + + set_display + Data Processing --------------- diff --git a/pygmt/__init__.py b/pygmt/__init__.py index 033a21f631b..1dc2ed79ac4 100644 --- a/pygmt/__init__.py +++ b/pygmt/__init__.py @@ -25,7 +25,7 @@ # Import modules to make the high-level GMT Python API from pygmt import datasets from pygmt.accessors import GMTDataArrayAccessor -from pygmt.figure import Figure +from pygmt.figure import Figure, set_display from pygmt.session_management import begin as _begin from pygmt.session_management import end as _end from pygmt.src import ( diff --git a/pygmt/figure.py b/pygmt/figure.py index db152e0d27c..ef9269a5994 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -6,9 +6,10 @@ from tempfile import TemporaryDirectory try: - from IPython.display import Image -except ImportError: - Image = None + import IPython +except KeyError: + IPython = None # pylint: disable=invalid-name + from pygmt.clib import Session from pygmt.exceptions import GMTError, GMTInvalidInput @@ -25,6 +26,23 @@ # This is needed for the sphinx-gallery scraper in pygmt/sphinx_gallery.py SHOWED_FIGURES = [] +# Configurations for figure display +SHOW_CONFIG = { + "method": "external", # Open in an external viewer by default +} + +# Show figures in Jupyter notebooks if available +if IPython: + get_ipython = IPython.get_ipython() # pylint: disable=invalid-name + if get_ipython and "IPKernelApp" in get_ipython.config: # Jupyter Notebook enabled + SHOW_CONFIG["method"] = "notebook" + +# Set environment variable PYGMT_USE_EXTERNAL_DISPLAY to 'false' to disable +# external display. Use it when running the tests and building the docs to +# avoid popping up windows. +if os.environ.get("PYGMT_USE_EXTERNAL_DISPLAY", "true").lower() == "false": + SHOW_CONFIG["method"] = "none" + class Figure: """ @@ -235,62 +253,72 @@ def savefig( if show: launch_external_viewer(fname) - def show(self, dpi=300, width=500, method="static"): + def show(self, dpi=300, width=500, method=None): """ Display a preview of the figure. - Inserts the preview in the Jupyter notebook output. You will need to - have IPython installed for this to work. You should have it if you are - using the notebook. + Inserts the preview in the Jupyter notebook output if available, + otherwise opens it in the default viewer for your operating system + (falls back to the default web browser). + + :func:`pygmt.set_display` can select the default display method + (**notebook**, **external**, or **none**). + + The ``method`` parameter can also override the default display method + for the current figure. Parameters ``dpi`` and ``width`` can be used + to control the resolution and dimension of the figure in the notebook. + + Note: The external viewer can be disabled by setting the + PYGMT_USE_EXTERNAL_DISPLAY environment variable to **false**. + This is useful when running unit tests and building the documentation + in consoles without a Graphical User Interface. - If ``method='external'``, makes PDF preview instead and opens it in the - default viewer for your operating system (falls back to the default web - browser). Note that the external viewer does not block the current - process, so this won't work in a script. + Note that the external viewer does not block the current process. Parameters ---------- dpi : int - The image resolution (dots per inch). + The image resolution (dots per inch) in Jupyter notebooks. width : int - Width of the figure shown in the notebook in pixels. Ignored if - ``method='external'``. + The image width (in pixels) in Jupyter notebooks. method : str - How the figure will be displayed. Options are (1) ``'static'``: PNG - preview (default); (2) ``'external'``: PDF preview in an external - program. + How the current figure will be displayed. Options are - Returns - ------- - img : IPython.display.Image - Only if ``method != 'external'``. + - **external**: PDF preview in an external program [default] + - **notebook**: PNG preview [default in Jupyter notebooks] + - **none**: Disable image preview """ # Module level variable to know which figures had their show method # called. Needed for the sphinx-gallery scraper. SHOWED_FIGURES.append(self) - if method not in ["static", "external"]: - raise GMTInvalidInput("Invalid show method '{}'.".format(method)) - if method == "external": - pdf = self._preview(fmt="pdf", dpi=dpi, anti_alias=False, as_bytes=False) - launch_external_viewer(pdf) - img = None - elif method == "static": - png = self._preview( - fmt="png", dpi=dpi, anti_alias=True, as_bytes=True, transparent=True + # Set the display method + if method is None: + method = SHOW_CONFIG["method"] + + if method not in ["external", "notebook", "none"]: + raise GMTInvalidInput( + ( + f"Invalid display method '{method}', " + "should be either 'notebook', 'external', or 'none'." + ) ) - if Image is None: + + if method in ["notebook", "none"]: + if IPython is None: raise GMTError( - " ".join( - [ - "Cannot find IPython.", - "Make sure you have it installed", - "or use 'method=\"external\"' to open in an external viewer.", - ] + ( + "Notebook display is selected, but IPython is not available. " + "Make sure you have IPython installed, " + "or run the script in a Jupyter notebook." ) ) - img = Image(data=png, width=width) - return img + png = self._preview(fmt="png", dpi=dpi, anti_alias=True, as_bytes=True) + IPython.display.display(IPython.display.Image(data=png, width=width)) + + if method == "external": + pdf = self._preview(fmt="pdf", dpi=dpi, anti_alias=False, as_bytes=False) + launch_external_viewer(pdf) def shift_origin(self, xshift=None, yshift=None): """ @@ -396,3 +424,27 @@ def _repr_html_(self): subplot, text, ) + + +def set_display(method=None): + """ + Set the display method. + + Parameters + ---------- + method : str or None + The method to display an image. Choose from: + + - **external**: PDF preview in an external program [default] + - **notebook**: PNG preview [default in Jupyter notebooks] + - **none**: Disable image preview + """ + if method in ["notebook", "external", "none"]: + SHOW_CONFIG["method"] = method + elif method is not None: + raise GMTInvalidInput( + ( + f"Invalid display mode '{method}', " + "should be either 'notebook', 'external' or 'none'." + ) + ) diff --git a/pygmt/tests/test_figure.py b/pygmt/tests/test_figure.py index 7b771b5a9d8..213c0048d3f 100644 --- a/pygmt/tests/test_figure.py +++ b/pygmt/tests/test_figure.py @@ -8,7 +8,7 @@ import numpy as np import numpy.testing as npt import pytest -from pygmt import Figure +from pygmt import Figure, set_display from pygmt.exceptions import GMTInvalidInput @@ -142,8 +142,7 @@ def test_figure_show(): """ fig = Figure() fig.basemap(region="10/70/-300/800", projection="X3i/5i", frame="af") - img = fig.show(width=800) - assert img.width == 800 + fig.show() @pytest.mark.mpl_image_compare @@ -175,3 +174,12 @@ def test_figure_show_invalid_method(): fig.basemap(region="10/70/-300/800", projection="X3i/5i", frame="af") with pytest.raises(GMTInvalidInput): fig.show(method="test") + + +def test_figure_set_display_invalid(): + """ + Test to check if an error is raised when an invalid method is passed to + set_display. + """ + with pytest.raises(GMTInvalidInput): + set_display(method="invalid")