diff --git a/Makefile b/Makefile index 072b8ccdb50..9a4069ad80e 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ test: @echo "" @cd $(TESTDIR); python -c "import $(PROJECT); $(PROJECT).print_clib_info()" @echo "" - cd $(TESTDIR); pytest $(PYTEST_ARGS) $(PROJECT) + cd $(TESTDIR); PYGMT_EXTERNAL_VIEWER='false' pytest $(PYTEST_ARGS) $(PROJECT) cp $(TESTDIR)/.coverage* . rm -r $(TESTDIR) diff --git a/doc/Makefile b/doc/Makefile index d310fc02cbb..8f807a21ee5 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -2,7 +2,7 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = sphinx-build +SPHINXBUILD = PYGMT_EXTERNAL_VIEWER='false' sphinx-build SPHINXAUTOGEN = sphinx-autogen BUILDDIR = _build @@ -45,17 +45,8 @@ api: @echo $(SPHINXAUTOGEN) -i -t _templates -o api/generated api/*.rst - linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -serve: - cd $(BUILDDIR)/html && python -m http.server 8009 diff --git a/doc/api/index.rst b/doc/api/index.rst index 3b2a4aeab6d..a50adfe252b 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -41,6 +41,13 @@ Saving and displaying the figure: Figure.show Figure.psconvert +Utilities for working in Jupyter notebooks: + +.. autosummary:: + :toctree: generated + + enable_notebook + Data Processing --------------- diff --git a/doc/index.rst b/doc/index.rst index 00050a3ff20..acf15ba5643 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -32,6 +32,7 @@ projections/index.rst tutorials/coastlines.rst tutorials/plot.rst + notebook-support.ipynb .. toctree:: :maxdepth: 2 diff --git a/doc/notebook-support.ipynb b/doc/notebook-support.ipynb new file mode 100644 index 00000000000..4a25781fc79 --- /dev/null +++ b/doc/notebook-support.ipynb @@ -0,0 +1,118 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Jupyter notebook support\n", + "\n", + "PyGMT includes support for displaying your figures on [Jupyter notebooks](https://jupyter.org/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pygmt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Figures can be displayed inline in the notebook by placing the [pygmt.Figure](api/generated/pygmt.Figure.rst) object at the end of a cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = pygmt.Figure()\n", + "fig.basemap(region=\"g\", projection=\"W10i\", frame=True)\n", + "fig.grdimage(\"@earth_relief_60m\", cmap=\"geo\")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or by calling [pygmt.Figure.show](api/generated/pygmt.Figure.show.rst). In this case, you might not want a figure to pop-up in an external window every time. To enable notebook support and prevent pop-up figures, use [pygmt.enable_notebook](api/generated/pygmt.enable_notebook.rst)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pygmt.enable_notebook()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = pygmt.Figure()\n", + "fig.basemap(region=\"g\", projection=\"W10i\", frame=True)\n", + "fig.coast(land=\"#666666\", water=\"skyblue\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can control the resolution of the images inserted into the notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pygmt.enable_notebook(dpi=70)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = pygmt.Figure()\n", + "fig.basemap(region=\"g\", projection=\"W10i\", frame=True)\n", + "fig.coast(land=\"#666666\", water=\"skyblue\")\n", + "fig.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pygmt/__init__.py b/pygmt/__init__.py index 9c40f485c36..1de65ef7fe4 100644 --- a/pygmt/__init__.py +++ b/pygmt/__init__.py @@ -13,7 +13,7 @@ # Import modules to make the high-level GMT Python API from .session_management import begin as _begin, end as _end -from .figure import Figure +from .figure import Figure, enable_notebook from .gridding import surface from .modules import info, grdinfo, which from . import datasets diff --git a/pygmt/figure.py b/pygmt/figure.py index eb00a27e698..6b2944626a8 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -3,16 +3,17 @@ """ import os from tempfile import TemporaryDirectory -import base64 try: - from IPython.display import Image + import IPython + + HAS_IPYTHON = True except ImportError: - Image = None + HAS_IPYTHON = False from .clib import Session from .base_plotting import BasePlotting -from .exceptions import GMTError, GMTInvalidInput +from .exceptions import GMTInvalidInput from .helpers import ( build_arg_string, fmt_docstring, @@ -26,6 +27,31 @@ # A registry of all figures that have had "show" called in this session. # This is needed for the sphinx-gallery scraper in pygmt/sphinx_gallery.py SHOWED_FIGURES = [] +# Configuration options for Jupyter notebook support +SHOW_CONFIG = {"dpi": 200, "external": True, "display": False} +# If the environment variable is set to "false", disable the external viewer. Use this +# for running the tests and building the docs to avoid pop up windows. +if os.environ.get("PYGMT_EXTERNAL_VIEWER", "default") == "false": + SHOW_CONFIG["external"] = False + + +def enable_notebook(dpi=200): + """ + Enable extended support for the Jupyter notebook. + + Suppresses an external window from popping-up when :meth:`pygmt.Figure.show` is + called. Can also control the resolution of displayed images in the notebook. + + Parameters + ---------- + dpi : int + Set the default DPI (dots-per-inch) used for PNG image previews that are + inserted into the notebook. + + """ + SHOW_CONFIG["dpi"] = dpi + SHOW_CONFIG["external"] = False + SHOW_CONFIG["display"] = True class Figure(BasePlotting): @@ -164,9 +190,7 @@ def psconvert(self, **kwargs): with Session() as lib: lib.call_module("psconvert", build_arg_string(kwargs)) - def savefig( - self, fname, transparent=False, crop=True, anti_alias=True, show=False, **kwargs - ): + def savefig(self, fname, transparent=False, crop=True, anti_alias=True, **kwargs): """ Save the figure to a file. @@ -177,8 +201,8 @@ def savefig( BMP (``.bmp``), TIFF (``.tif``), EPS (``.eps``), and KML (``.kml``). The KML output generates a companion PNG file. - You can pass in any keyword arguments that - :meth:`~gmt.Figure.psconvert` accepts. + You can pass in any keyword arguments that :meth:`~gmt.Figure.psconvert` + accepts. Parameters ---------- @@ -195,8 +219,6 @@ def savefig( JPG, TIf). More specifically, uses options ``Qt=2, Qg=2`` in :meth:`~gmt.Figure.psconvert`. Ignored if creating vector graphics. Overrides values of ``Qt`` and ``Qg`` passed in through ``kwargs``. - show: bool - If True, will open the figure in an external viewer. dpi : int Set raster resolution in dpi. Default is 720 for PDF, 300 for others. @@ -223,66 +245,41 @@ def savefig( kwargs["W"] = "+k" self.psconvert(prefix=prefix, fmt=fmt, crop=crop, **kwargs) - if show: - launch_external_viewer(fname) - def show(self, dpi=300, width=500, method="static"): + def show(self): """ 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. + By default, opens a PDF preview of the figure in the default PDF viewer. Behaves + differently depending on the operating system: - 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. + * Linux: Uses ``xdg-open`` (which might need to be installed). + * Mac: Uses the ``open`` command. + * Windows: Uses Python's :func:`os.startfile` function. - Parameters - ---------- - dpi : int - The image resolution (dots per inch). - width : int - Width of the figure shown in the notebook in pixels. Ignored if - ``method='external'``. - method : str - How the figure will be displayed. Options are (1) ``'static'``: PNG - preview (default); (2) ``'external'``: PDF preview in an external - program. + If we can't determine your OS or ``xdg-open`` is not available on Linux, falls + back to using the default web browser to open the file. - Returns - ------- - img : IPython.display.Image - Only if ``method != 'external'``. + If :func:`pygmt.enable_notebook` was called, will not open the external viewer + and will instead use ``IPython.display.display`` to display the figure on th + Jupyter notebook or IPython Qt console. + The external viewer can also be disabled by setting the + ``PYGMT_EXTERNAL_VIEWER`` environment variable to ``false``. This is mainly used + for running our tests and building the documentation. """ # 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 + if SHOW_CONFIG["external"]: + pdf = self._preview( + fmt="pdf", dpi=SHOW_CONFIG["dpi"], anti_alias=False, as_bytes=False ) - if Image is None: - raise GMTError( - " ".join( - [ - "Cannot find IPython.", - "Make sure you have it installed", - "or use 'external=True' to open in an external viewer.", - ] - ) - ) - img = Image(data=png, width=width) - return img + launch_external_viewer(pdf) + if HAS_IPYTHON and SHOW_CONFIG["display"]: + png = self._repr_png_() + IPython.display.display(IPython.display.Image(data=png)) def shift_origin(self, xshift=None, yshift=None): """ @@ -351,15 +348,7 @@ def _repr_png_(self): Show a PNG preview if the object is returned in an interactive shell. For the Jupyter notebook or IPython Qt console. """ - png = self._preview(fmt="png", dpi=70, anti_alias=True, as_bytes=True) + png = self._preview( + fmt="png", dpi=SHOW_CONFIG["dpi"], anti_alias=True, as_bytes=True + ) return png - - def _repr_html_(self): - """ - Show the PNG image embedded in HTML with a controlled width. - Looks better than the raw PNG. - """ - raw_png = self._preview(fmt="png", dpi=300, anti_alias=True, as_bytes=True) - base64_png = base64.encodebytes(raw_png) - html = '' - return html.format(image=base64_png.decode("utf-8"), width=500) diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index e753223f974..989cca2ac7f 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -1,6 +1,7 @@ """ Utilities and common tasks for wrapping the GMT modules. """ +import os import sys import shutil import subprocess @@ -210,11 +211,15 @@ def launch_external_viewer(fname): # with noise run_args = dict(stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + platform = sys.platform + # Open the file with the default viewer. # Fall back to the browser if can't recognize the operating system. - if sys.platform.startswith("linux") and shutil.which("xdg-open"): + if platform.startswith("linux") and shutil.which("xdg-open"): subprocess.run(["xdg-open", fname], **run_args) - elif sys.platform == "darwin": # Darwin is macOS + elif platform == "darwin": # Darwin is macOS subprocess.run(["open", fname], **run_args) + elif platform == "win32": + os.startfile(fname) # pylint: disable=no-member else: webbrowser.open_new_tab("file://{}".format(fname)) diff --git a/pygmt/tests/test_figure.py b/pygmt/tests/test_figure.py index a2db90dfc06..38fe08c1b0a 100644 --- a/pygmt/tests/test_figure.py +++ b/pygmt/tests/test_figure.py @@ -110,11 +110,12 @@ def mock_psconvert(*args, **kwargs): # pylint: disable=unused-argument def test_figure_show(): - "Test that show creates the correct file name and deletes the temp dir" + "Test that show triggers the correct actions" + # Check if the external viewer is triggered using monkeypatching fig = Figure() - fig.basemap(R="10/70/-300/800", J="X3i/5i", B="af") - img = fig.show(width=800) - assert img.width == 800 + fig.basemap(region="10/70/-300/800", projection="X3i/5i", frame="af") + res = fig.show() + assert res is fig @pytest.mark.mpl_image_compare