Skip to content

Commit 76d3aae

Browse files
larsonerclaude
andcommitted
fix: usable repr sizing and themable embeds for sphinx-gallery
- The _repr_html_ fallback (hit when the default renderer is not a mimetype renderer, e.g. sphinx_gallery_png) now sizes like the html renderers (default_height=525) instead of height:100%, which collapses or overflows in containers with no set height. - The scraper now embeds shown figures inline in the rst, wrapped in the same output_subarea div sphinx-gallery wraps captured HTML reprs in, so themes can style both kinds of embed with one hook; it no longer writes .html files next to the images. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0795b26 commit 76d3aae

3 files changed

Lines changed: 52 additions & 66 deletions

File tree

‎plotly/basedatatypes.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,14 @@ def _repr_html_(self):
825825
if "text/html" in bundle:
826826
return bundle["text/html"]
827827
else:
828-
return self.to_html(full_html=False, include_plotlyjs="cdn")
828+
# Size like the html renderers do: "100%" height collapses or
829+
# overflows in plain-HTML consumers such as sphinx-gallery.
830+
return self.to_html(
831+
full_html=False,
832+
include_plotlyjs="cdn",
833+
default_width="100%",
834+
default_height=525,
835+
)
829836

830837
def _repr_mimebundle_(self, include=None, exclude=None, validate=True, **kwargs):
831838
"""

‎plotly/io/_sg_scraper.py‎

Lines changed: 27 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ def plotly_sg_scraper(block, block_vars, gallery_conf, **kwargs):
2323
``sphinx_gallery_png`` renderer, which is made the default renderer as a
2424
side effect of importing this module.
2525
26-
Every figure shown that way is written to the gallery image directory
27-
twice: once as an interactive HTML file, which is embedded in the page,
28-
and once as a static image, which sphinx-gallery uses to generate the
29-
thumbnail of the example.
26+
Every figure shown that way is embedded in the page as interactive HTML,
27+
and written to the gallery image directory as a static image, which
28+
sphinx-gallery uses to generate the thumbnail of the example.
3029
3130
A figure that is instead displayed by making it the last expression of a
3231
code block (sphinx-gallery's repr capture) gets a static image too, so
@@ -72,35 +71,20 @@ def plotly_sg_scraper(block, block_vars, gallery_conf, **kwargs):
7271
if fig_dict not in sphinx_gallery_figures:
7372
figures.append((fig_dict, False))
7473
try:
75-
if not _static_export_available():
76-
# No images means no thumbnails, and sphinx-gallery requires an
77-
# image for every path taken from the iterator, so embed shown
78-
# figures inline instead of via files.
79-
return "".join(
80-
_inline_html(fig_dict) for fig_dict, shown in figures if shown
81-
)
82-
html_names = []
83-
for (fig_dict, shown), image_path in zip(figures, image_path_iterator):
84-
# sphinx-gallery hands out one path per image; the HTML file sits
85-
# next to the image it is the interactive counterpart of.
86-
path_root = os.path.splitext(image_path)[0]
87-
_write_image(fig_dict, f"{path_root}.{image_format}", image_format)
88-
if not shown:
89-
# Repr-displayed: sphinx-gallery embeds the HTML itself, the
90-
# static image only makes the figure available as a thumbnail.
91-
continue
92-
plotly.io.write_html(
93-
fig_dict,
94-
file=f"{path_root}.html",
95-
include_plotlyjs="cdn",
96-
full_html=False,
97-
default_width="100%",
98-
default_height=525,
99-
validate=False,
100-
)
101-
html_names.append(f"{path_root}.html")
102-
# Use the `figure_rst` helper function to generate rST for image files
103-
return figure_rst(html_names, gallery_conf["src_dir"])
74+
export_available = _static_export_available()
75+
rst = ""
76+
for fig_dict, shown in figures:
77+
if export_available:
78+
# sphinx-gallery requires an image at every path taken from
79+
# the iterator, so don't consume paths when export failed.
80+
image_path = next(image_path_iterator)
81+
path_root = os.path.splitext(image_path)[0]
82+
_write_image(fig_dict, f"{path_root}.{image_format}", image_format)
83+
if shown:
84+
# Repr-displayed figures are embedded by sphinx-gallery
85+
# itself; their static image only serves as the thumbnail.
86+
rst += _inline_html(fig_dict)
87+
return rst
10488
finally:
10589
# Don't let figures leak into the next block if writing one failed.
10690
del sphinx_gallery_figures[:]
@@ -158,7 +142,11 @@ def _static_export_available():
158142

159143

160144
def _inline_html(fig_dict):
161-
"""Embed a figure into the rst directly, rather than via a file."""
145+
"""Embed a figure into the rst directly, rather than via a file.
146+
147+
The figure is wrapped in the same div that sphinx-gallery wraps captured
148+
HTML reprs in, so that themes can style both kinds of embed alike.
149+
"""
162150
html = plotly.io.to_html(
163151
fig_dict,
164152
include_plotlyjs="cdn",
@@ -167,6 +155,11 @@ def _inline_html(fig_dict):
167155
default_height=525,
168156
validate=False,
169157
)
158+
html = (
159+
'<div class="output_subarea output_html rendered_html output_result">\n'
160+
f"{html}\n"
161+
"</div>"
162+
)
170163
return "\n.. raw:: html\n\n" + textwrap.indent(html, " ") + "\n"
171164

172165

@@ -182,31 +175,3 @@ def _write_image(fig_dict, file, image_format):
182175
) from exc
183176

184177

185-
def figure_rst(figure_list, sources_dir):
186-
"""Generate RST for a list of HTML filenames.
187-
188-
Parameters
189-
----------
190-
figure_list : list
191-
List of strings of the figures' absolute paths.
192-
sources_dir : str
193-
absolute path of Sphinx documentation sources (unused, kept for
194-
compatibility with the equivalent sphinx-gallery helper)
195-
196-
Returns
197-
-------
198-
images_rst : str
199-
rst code to embed the images in the document
200-
"""
201-
# The HTML files live in the "images" directory next to the document that
202-
# includes them, so the paths are relative to that document.
203-
return "".join(
204-
SINGLE_HTML % ("images/" + os.path.basename(figure_path))
205-
for figure_path in figure_list
206-
)
207-
208-
209-
SINGLE_HTML = """
210-
.. raw:: html
211-
:file: %s
212-
"""

‎tests/test_io/test_sg_scraper.py‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def thumbnail(**file_conf):
120120

121121
@pytest.mark.parametrize("image_format", ["png", "svg"])
122122
def test_scraper(gallery, image_format):
123-
"""Each shown figure gets an image and an HTML file, and can be a thumbnail."""
123+
"""Each shown figure gets an embed and a static image, and can be a thumbnail."""
124124
# Selecting a format by wrapping the scraper is the approach documented at
125125
# https://sphinx-gallery.github.io/stable/advanced.html#example-3-matplotlib-with-svg-format
126126
gallery.conf["image_scrapers"] = (
@@ -135,10 +135,11 @@ def test_scraper(gallery, image_format):
135135
assert len(gallery.paths) == 2
136136
for path, color in zip(gallery.paths, COLORS):
137137
root = os.path.splitext(path)[0]
138-
assert os.path.isfile(f"{root}.html")
139-
assert f"images/{os.path.basename(root)}.html" in rst
140138
assert_image_color(Path(f"{root}.{image_format}"), color, image_format)
141139
assert rst.count(".. raw:: html") == 2
140+
assert rst.count("plotly-graph-div") == 2
141+
# The wrapper sphinx-gallery uses for HTML reprs, so themes can style both
142+
assert rst.count('class="output_subarea') == 2
142143

143144
# The thumbnail must be a scraped figure rather than a "no image" default,
144145
# and one image per figure in order is what makes `thumbnail_number` work
@@ -249,6 +250,19 @@ def raise_no_browser(*args, **kwargs):
249250
assert len([r for r in caplog.records if "Kaleido" in r.getMessage()]) == 1
250251

251252

253+
def test_repr_html_fallback_size(monkeypatch):
254+
"""The repr of a figure must have a usable pixel height, not "100%".
255+
256+
With the (non-mimetype) sphinx_gallery_png renderer selected, sphinx-
257+
gallery captures ``_repr_html_``, whose output ends up in a container
258+
with no set height.
259+
"""
260+
monkeypatch.setattr(pio.renderers, "default", "sphinx_gallery_png")
261+
assert 'style="height:525px; width:100%;"' in go.Figure()._repr_html_()
262+
fig = go.Figure(layout={"height": 400})
263+
assert 'style="height:400px; width:100%;"' in fig._repr_html_()
264+
265+
252266
def test_image_scrapers_by_name(monkeypatch):
253267
"""`image_scrapers=("plotly",)` must resolve through sphinx-gallery."""
254268
from sphinx_gallery.gen_rst import _get_callables

0 commit comments

Comments
 (0)