diff --git a/plotly/io/__init__.py b/plotly/io/__init__.py index c0478c70ac0..7b26dbfb297 100644 --- a/plotly/io/__init__.py +++ b/plotly/io/__init__.py @@ -5,8 +5,9 @@ from ._templates import templates, to_templated +from ._html import to_html, write_html + from ._renderers import renderers, show from . import base_renderers -from ._html import to_html, write_html diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index bfe7bb01fdb..652bbc817ad 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -6,10 +6,11 @@ import os import six -from plotly.io import to_json, to_image +from plotly.io import to_json, to_image, write_image, write_html from plotly import utils, optional_imports from plotly.io._orca import ensure_server from plotly.offline.offline import _get_jconfig, get_plotlyjs +from plotly.tools import return_figure_from_figure_or_data ipython_display = optional_imports.get_module('IPython.display') IPython = optional_imports.get_module('IPython') @@ -637,3 +638,20 @@ def render(self, fig_dict): validate=False, ) open_html_in_browser(html, self.using, self.new, self.autoraise) + + +class SphinxGalleryRenderer(ExternalRenderer): + + def render(self, fig_dict): + stack = inspect.stack() + # Name of script from which plot function was called is retrieved + try: + filename = stack[3].filename # let's hope this is robust... + except: #python 2 + filename = stack[3][1] + filename_root, _ = os.path.splitext(filename) + filename_html = filename_root + '.html' + filename_png = filename_root + '.png' + figure = return_figure_from_figure_or_data(fig_dict, True) + _ = write_html(fig_dict, file=filename_html) + write_image(figure, filename_png) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index a396e54c817..d2aefbe5a0c 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -12,7 +12,8 @@ from plotly.io._base_renderers import ( MimetypeRenderer, ExternalRenderer, PlotlyRenderer, NotebookRenderer, KaggleRenderer, ColabRenderer, JsonRenderer, PngRenderer, JpegRenderer, - SvgRenderer, PdfRenderer, BrowserRenderer, IFrameRenderer) + SvgRenderer, PdfRenderer, BrowserRenderer, IFrameRenderer, + SphinxGalleryRenderer) from plotly.io._utils import validate_coerce_fig_to_dict ipython = optional_imports.get_module('IPython') @@ -394,6 +395,7 @@ def show(fig, renderer=None, validate=True, **kwargs): renderers['chrome'] = BrowserRenderer(config=config, using='chrome') renderers['chromium'] = BrowserRenderer(config=config, using='chromium') renderers['iframe'] = IFrameRenderer(config=config) +renderers['sphinx_gallery'] = SphinxGalleryRenderer() # Set default renderer # -------------------- diff --git a/plotly/io/_sg_scraper.py b/plotly/io/_sg_scraper.py new file mode 100644 index 00000000000..5e57c85a5f0 --- /dev/null +++ b/plotly/io/_sg_scraper.py @@ -0,0 +1,104 @@ +# This module defines an image scraper for sphinx-gallery +# https://sphinx-gallery.github.io/ +# which can be used by projects using plotly in their documentation. +import inspect, os + +import plotly +from glob import glob +import shutil + +plotly.io.renderers.default = 'sphinx_gallery' + + +def plotly_sg_scraper(block, block_vars, gallery_conf, **kwargs): + """Scrape Plotly figures for galleries of examples using + sphinx-gallery. + + Examples should use ``plotly.io.show()`` to display the figure with + the custom sphinx_gallery renderer. + + Since the sphinx_gallery renderer generates both html and static png + files, we simply crawl these files and give them the appropriate path. + + Parameters + ---------- + block : tuple + A tuple containing the (label, content, line_number) of the block. + block_vars : dict + Dict of block variables. + gallery_conf : dict + Contains the configuration of Sphinx-Gallery + **kwargs : dict + Additional keyword arguments to pass to + :meth:`~matplotlib.figure.Figure.savefig`, e.g. ``format='svg'``. + The ``format`` kwarg in particular is used to set the file extension + of the output file (currently only 'png' and 'svg' are supported). + + Returns + ------- + rst : str + The ReSTructuredText that will be rendered to HTML containing + the images. + + Notes + ----- + Add this function to the image scrapers + """ + examples_dirs = gallery_conf['examples_dirs'] + if isinstance(examples_dirs, (list, tuple)): + examples_dirs = examples_dirs[0] + pngs = sorted(glob(os.path.join(examples_dirs, + '*.png'))) + htmls = sorted(glob(os.path.join(examples_dirs, + '*.html'))) + image_path_iterator = block_vars['image_path_iterator'] + image_names = list() + seen = set() + for html, png in zip(htmls, pngs): + if png not in seen: + seen |= set(png) + this_image_path_png = next(image_path_iterator) + this_image_path_html = (os.path.splitext( + this_image_path_png)[0] + '.html') + image_names.append(this_image_path_html) + shutil.move(png, this_image_path_png) + shutil.move(html, this_image_path_html) + # Use the `figure_rst` helper function to generate rST for image files + return figure_rst(image_names, gallery_conf['src_dir']) + + +def figure_rst(figure_list, sources_dir): + """Generate RST for a list of PNG filenames. + + Depending on whether we have one or more figures, we use a + single rst call to 'image' or a horizontal list. + + Parameters + ---------- + figure_list : list + List of strings of the figures' absolute paths. + sources_dir : str + absolute path of Sphinx documentation sources + + Returns + ------- + images_rst : str + rst code to embed the images in the document + """ + + figure_paths = [os.path.relpath(figure_path, sources_dir) + .replace(os.sep, '/').lstrip('/') + for figure_path in figure_list] + images_rst = "" + figure_name = figure_paths[0] + ext = os.path.splitext(figure_name)[1] + figure_path = os.path.join('images', os.path.basename(figure_name)) + images_rst = SINGLE_HTML % figure_path + return images_rst + + +SINGLE_HTML = """ +.. raw:: html + :file: %s +""" + diff --git a/plotly/tests/test_orca/test_sg_scraper.py b/plotly/tests/test_orca/test_sg_scraper.py new file mode 100644 index 00000000000..e53ab6b59d7 --- /dev/null +++ b/plotly/tests/test_orca/test_sg_scraper.py @@ -0,0 +1,60 @@ +import plotly +import os +import shutil +import pytest + + +# Fixtures +# -------- +@pytest.fixture() +def setup(): + # Reset orca state + plotly.io.orca.config.restore_defaults(reset_server=False) + + +# Run setup before every test function in this file +pytestmark = pytest.mark.usefixtures("setup") + + +def execute_plotly_example(): + """ + Some typical code which would go inside a gallery example. + """ + import plotly.graph_objs as go + + # Create random data with numpy + import numpy as np + + N = 200 + random_x = np.random.randn(N) + random_y_0 = np.random.randn(N) + random_y_1 = np.random.randn(N) - 1 + + # Create traces + trace_0 = go.Scatter( + x=random_x, + y=random_y_0, + mode='markers', + name='Above', + ) + + fig = go.Figure(data=[trace_0]) + plotly.io.show(fig) + + +def test_scraper(): + from plotly.io._sg_scraper import plotly_sg_scraper + # test that monkey-patching worked ok + assert plotly.io.renderers.default == 'sphinx_gallery' + # Use dummy values for arguments of plotly_sg_scraper + block = '' # we don't need actually code + import tempfile + tempdir = tempfile.mkdtemp() + gallery_conf = {'src_dir':tempdir, + 'examples_dirs':'plotly/tests/test_orca'} + names = iter(['0', '1', '2']) + block_vars = {'image_path_iterator':names} + execute_plotly_example() + res = plotly_sg_scraper(block, block_vars, gallery_conf) + shutil.rmtree(tempdir) + assert ".. raw:: html" in res diff --git a/tox.ini b/tox.ini index 4a8d0d19135..1b6bd10c38d 100644 --- a/tox.ini +++ b/tox.ini @@ -74,6 +74,7 @@ deps= optional: matplotlib==2.2.3 optional: xarray==0.10.9 optional: scikit-image==0.13.1 + optional: psutil==5.6.2 plot_ly: pandas==0.23.2 plot_ly: numpy==1.14.3 plot_ly: ipywidgets==7.2.0