8000 [ENH] Add option to render multiple images from same cell as single-i… · sphinx-gallery/sphinx-gallery@e7b7603 · GitHub
[go: up one dir, main page]

Skip to content

Commit e7b7603

Browse files
authored
[ENH] Add option to render multiple images from same cell as single-img (#1384)
1 parent 840f3a1 commit e7b7603

File tree

9 files changed

+245
-21
lines changed

9 files changed

+245
-21
lines changed

doc/configuration.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,12 @@ Some options can also be set or overridden on a file-by-file basis:
103103
- ``# sphinx_gallery_failing_thumbnail`` (:ref:`failing_thumbnail`)
104104
- ``# sphinx_gallery_dummy_images`` (:ref:`dummy_images`)
105105
- ``# sphinx_gallery_capture_repr`` (:ref:`capture_repr`)
106+
- ``# sphinx_gallery_multi_image`` (:ref:`multi_image`)
106107

107108
Some options can be set on a per-code-block basis in a file:
108109

109110
- ``# sphinx_gallery_defer_figures`` (:ref:`defer_figures`)
111+
- ``# sphinx_gallery_multi_image_block`` (:ref:`multi_image`)
110112

111113
Some options can be set on a per-line basis in a file:
112114
- ``# sphinx_gallery_start_ignore`` and ``# sphinx_gallery_end_ignore`` (:ref:`hiding_code_blocks`)
@@ -1914,6 +1916,44 @@ further deferred, if desired). The following produces only one plot::
19141916
plt.plot([2, 2])
19151917
plt.show()
19161918

1919+
.. _multi_image:
1920+
1921+
Controlling the layout of multiple figures from the same code block
1922+
===================================================================
1923+
1924+
By default, multiple figures generated from the same code block are stacked
1925+
side-by-side. Particularly for wide figures, this can lead to cases where images are
1926+
highly shrunk, losing their legibility. This behaviour can be controlled using two
1927+
optional variables:
1928+
1929+
- a file-wide ``sphinx_gallery_multi_image`` variable
1930+
- a code block-specific ``sphinx_gallery_multi_image_block`` variable
1931+
1932+
The default behaviour is to treat these variables as being set to ``"multi"``, which
1933+
causes figures to be stacked side-by-side. Setting these variables to ``"single"`` will
1934+
allow figures produced from a code block to be displayed as a single column.
1935+
1936+
For instance, adding::
1937+
1938+
# sphinx_gallery_multi_image = "single"
1939+
1940+
somewhere in an example file will cause images from all code blocks where multiple
1941+
figures are produced to be displayed in a single column.
1942+
1943+
Alternatively, adding::
1944+
1945+
# sphinx_gallery_multi_image_block = "single"
1946+
1947+
to a code block will cause multiple figures from only that code block to be displayed in
1948+
a single column.
1949+
1950+
Conversely, if ``sphinx_gallery_multi_image = "single"`` is set for the whole file,
1951+
adding ``sphinx_gallery_multi_image_block = "multi"`` can restore the default behaviour
1952+
for a single code block.
1953+
1954+
See the example :ref:`sphx_glr_auto_examples_plot_9_multi_image_separate.py` for a
1955+
demonstration of this functionality.
1956+
19171957
.. _hiding_code_blocks:
19181958

19191959
Hiding lines of code

examples/plot_1_exp.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
Plotting the exponential function
33
=================================
44
5-
This example demonstrates how to import a local module and how images are
6-
stacked when two plots are created in one code block. The variable ``N`` from
7-
the example 'Local module' (file ``local_module.py``) is imported in the code
8-
below. Further, note that when there is only one code block in an example, the
9-
output appears before the code block.
5+
This example demonstrates how to import a local module and how images are stacked when
6+
two plots are created in one code block (see the :doc:`plot_9_multi_image_separate`
7+
example for information on controlling this behaviour). The variable ``N`` from the
8+
example 'Local module' (file ``local_module.py``) is imported in the code below.
9+
Further, note that when there is only one code block in an example, the output appears
10+
before the code block.
1011
"""
1112

1213
# Code source: Óscar Nájera
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Force plots to be displayed on separate lines
3+
=============================================
4+
This example demonstrates how the visualisation of multiple plots produced from a single
5+
code block can be controlled. The default behaviour is to stack plots side-by-side,
6+
however this can be overridden to display each plot created by the code block on a
7+
separate line, preserving their size.
8+
9+
There are two config options to control this behaviour:
10+
11+
- a file-wide ``sphinx_gallery_multi_image`` variable
12+
- a code block-specific ``sphinx_gallery_multi_image_block`` variable
13+
14+
Setting these variables to ``"single"`` will force plots to be displayed on separate
15+
lines. Default behaviour is to treat these variables as being set to ``"multi"``.
16+
17+
Below we demonstrate how the file-wide ``sphinx_gallery_multi_image`` variable can be
18+
used to display plots on separate lines.
19+
"""
20+
21+
# Code source: Thomas S. Binns
22+
# License: BSD 3 clause
23+
24+
# sphinx_gallery_multi_image = "single"
25+
26+
import matplotlib.pyplot as plt
27+
import numpy as np
28+
29+
# %%
30+
31+
# Plots will be shown on separate lines
32+
33+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
34+
ax.pcolormesh(np.random.randn(100, 100))
35+
36+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
37+
ax.pcolormesh(np.random.randn(100, 100))
38+
39+
########################################################################################
40+
# Now, we show how the ``sphinx_gallery_multi_image_block`` variable can be used to
41+
# control the behaviour for a specific code block, here reverting to the default
42+
# behaviour of stacking plots side-by-side.
43+
44+
# %%
45+
46+
# sphinx_gallery_multi_image_block = "multi"
47+
# ↑↑↑ Return to default behaviour for just this cell
48+
49+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
50+
ax.pcolormesh(np.random.randn(100, 100))
51+
52+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
53+
ax.pcolormesh(np.random.randn(100, 100))

sphinx_gallery/gen_rst.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,12 +1000,21 @@ def execute_code_block(
10001000
sys.path.append(os.getcwd())
10011001

10021002
# Save figures unless there is a `sphinx_gallery_defer_figures` flag
1003-
match = re.search(
1003+
defer_figs_match = re.search(
10041004
r"^[\ \t]*#\s*sphinx_gallery_defer_figures[\ \t]*\n?",
10051005
block.content,
10061006
re.MULTILINE,
10071007
)
1008-
need_save_figures = match is None
1008+
need_save_figures = defer_figs_match is None
1009+
1010+
# Add `sphinx_gallery_multi_image_block` setting to block variables
1011+
# (extract config rather than just regex search since the option's value is needed)
1012+
script_vars["multi_image"] = py_source_parser.extract_file_config(
1013+
block.content
1014+
).get("multi_image_block")
1015+
1016+
# Add file_conf to script_vars to be read by image scrapers
1017+
script_vars["file_conf"] = file_conf
10091018

10101019
try:
10111020
# The "compile" step itself can fail on a SyntaxError, so just prepend

sphinx_gallery/scrapers.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,25 @@ def matplotlib_scraper(block, block_vars, gallery_conf, **kwargs):
204204
)
205205
)
206206

207+
# Determine whether single-img should be converted to multi-img
208+
convert_to_multi_image = True # default is to convert
209+
if block_vars.get("multi_image") is not None: # block setting takes precedence
210+
convert_to_multi_image = block_vars["multi_image"] != "single"
211+
elif block_vars.get("file_conf") is not None: # then file setting
212+
convert_to_multi_image = block_vars["file_conf"].get("multi_image") != "single"
213+
207214
plt.close("all")
208215
rst = ""
209216
if len(image_rsts) == 1:
210217
rst = image_rsts[0]
211218
elif len(image_rsts) > 1:
212-
image_rsts = [
213-
re.sub(r":class: sphx-glr-single-img", ":class: sphx-glr-multi-img", image)
214-
for image in image_rsts
215-
]
219+
if convert_to_multi_image:
220+
image_rsts = [
221+
re.sub(
222+
r":class: sphx-glr-single-img", ":class: sphx-glr-multi-img", image
223+
)
224+
for image in image_rsts
225+
]
216226
image_rsts = [
217227
HLIST_IMAGE_MATPLOTLIB + indent(image, " " * 6) for image in image_rsts
218228
]

sphinx_gallery/tests/test_full.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
#
4242
# total number of plot_*.py files in
4343
# (examples + examples_rst_index + examples_with_rst + examples_README_header)
44-
N_EXAMPLES = 17 + 3 + 2 + 1
44+
N_EXAMPLES = 19 + 3 + 2 + 1
4545
N_FAILING = 4
4646
N_GOOD = N_EXAMPLES - N_FAILING # galleries that run w/o error
4747
# passthroughs and non-executed examples in
@@ -409,6 +409,36 @@ def test_thumbnail_expected_failing_examples(sphinx_app, tmpdir):
409409
assert corr < 0.7 # i.e. thumbnail and "BROKEN" stamp are not identical
410410

411411

412+
def test_multi_image(sphinx_app):
413+
"""Test `sphinx_gallery_multi_image(_block)` variables."""
414+
generated_examples_dir = op.join(sphinx_app.outdir, "auto_examples")
415+
416+
# Check file-wide `sphinx_gallery_multi_image="single"` produces no multi-img
417+
html_fname = op.join(generated_examples_dir, "plot_multi_image_separate.html")
418+
with codecs.open(html_fname, "r", "utf-8") as fid:
419+
html = fid.read()
420+
assert "sphx-glr-single-img" in html
421+
assert "sphx-glr-multi-img" not in html
422+
423+
# Check block-specific `sphinx_gallery_multi_image_block` produces mixed img classes
424+
html_fname = op.join(generated_examples_dir, "plot_multi_image_block_separate.html")
425+
with codecs.open(html_fname, "r", "utf-8") as fid:
426+
html = fid.read()
427+
# find start of each code block
428+
matches = re.finditer('<div class="highlight-Python notranslate">', html)
429+
starts = [match.start() for match in matches] + [-1]
430+
assert len(starts) == 4 # 3 code block plus an extra for end index
431+
for block_idx, (start, end) in enumerate(zip(starts[:-1], starts[1:])):
432+
# ignore first code block (just imports)
433+
block_html = html[start:end]
434+
if block_idx == 1: # multi-img classes for this code block
435+
assert "sphx-glr-multi-img" in block_html
436+
assert "sphx-glr-single-img" not in block_html
437+
elif block_idx == 2: # single-img classes for this code block
438+
assert "sphx-glr-single-img" in block_html
439+
assert "sphx-glr-multi-img" not in block_html
440+
441+
412442
def test_command_line_args_img(sphinx_app):
413443
generated_examples_dir = op.join(sphinx_app.outdir, "auto_examples")
414444
thumb_fname = "../_images/sphx_glr_plot_command_line_args_thumb.png"
@@ -889,7 +919,7 @@ def test_rebuild(tmpdir_factory, sphinx_app):
889919
else:
890920
assert (
891921
re.match(
892-
".*[0|1] added, ([1-9]|10) changed, 0 removed$.*",
922+
".*[0|1] added, ([1-9]|1[0-1]) changed, 0 removed$.*",
893923
status,
894924
re.MULTILINE | re.DOTALL,
895925
)
@@ -1542,9 +1572,9 @@ def test_recommend_n_examples(sphinx_app):
15421572
assert '<p class="rubric">Related examples</p>' in html
15431573
assert count == n_examples
15441574
# Check the same 3 related examples are shown (can change when new examples added)
1545-
assert "sphx-glr-auto-examples-plot-repr-py" in html
1546-
assert "sphx-glr-auto-examples-plot-matplotlib-backend-py" in html
1547-
assert "sphx-glr-auto-examples-plot-second-future-imports-py" in html
1575+
assert "sphx-glr-auto-examples-plot-defer-figures-py" in html
1576+
assert "sphx-glr-auto-examples-plot-webp-py" in html
1577+
assert "sphx-glr-auto-examples-plot-command-line-args-py" in html
15481578

15491579

15501580
def test_sidebar_components_download_links(sphinx_app):

sphinx_gallery/tests/test_scrapers.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,15 @@ def test_save_matplotlib_figures(make_gallery_conf, ext):
6666
fname = gallery_conf["gallery_dir"] + fname
6767
assert os.path.isfile(fname)
6868

69+
def _create_two_images():
70+
image_path_iterator.next()
71+
image_path_iterator.next()
72+
plt.plot(1, 1)
73+
plt.figure()
74+
plt.plot(1, 1)
75+
6976
# Test capturing 2 images with shifted start number
70-
image_path_iterator.next()
71-
image_path_iterator.next()
72-
plt.plot(1, 1)
73-
plt.figure()
74-
plt.plot(1, 1)
77+
_create_two_images()
7578
image_rst = save_figures(block, block_vars, gallery_conf)
7679
assert len(image_path_iterator) == 5
7780
for ii in range(4, 6):
@@ -80,6 +83,33 @@ def test_save_matplotlib_figures(make_gallery_conf, ext):
8083
fname = gallery_conf["gallery_dir"] + fname
8184
assert os.path.isfile(fname)
8285

86+
# Test `sphinx_gallery_multi_image(_block)` variables work; these variables prevent
87+
# images with `sphx-glr-single-img` classes from being converted to
88+
# `sphx-glr-multi-img` classes; requires > 1 image
89+
90+
# Test file-wide `sphinx_gallery_multi_image` variable
91+
_create_two_images()
92+
block_vars["file_conf"] = {"multi_image": "single"}
93+
image_rst = save_figures(block, block_vars, gallery_conf)
94+
assert "sphx-glr-single-img" in image_rst
95+
assert "sphx-glr-multi-img" not in image_rst
96+
97+
# Test block-specific `sphinx_gallery_multi_image_block` variable
98+
# (test with default `sphinx_gallery_multi_image`, i.e. != "single")
99+
_create_two_images()
100+
block_vars["file_conf"] = {}
101+
block_vars["multi_image"] = "single"
102+
image_rst = save_figures(block, block_vars, gallery_conf)
103+
assert "sphx-glr-single-img" in image_rst
104+
assert "sphx-glr-multi-img" not in image_rst
105+
# (test block-specific setting overrides file-wide setting)
106+
_create_two_images()
107+
block_vars["file_conf"] = {"multi_image": "single"}
108+
block_vars["multi_image"] = "multi"
109+
image_rst = save_figures(block, block_vars, gallery_conf)
110+
assert "sphx-glr-single-img" not in image_rst
111+
assert "sphx-glr-multi-img" in image_rst
112+
83113

84114
def test_image_srcset_config(make_gallery_conf):
85115
with pytest.raises(ConfigError, match="'image_srcset' config allowed"):
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""
2+
Force images to be displayed on separate lines per block
3+
========================================================
4+
This example demonstrates how the ``sphinx_gallery_multi_image_block`` option can be
5+
used to force images to be displayed on separate lines for a specific block, rather than
6+
the default behaviour of displaying them side-by-side.
7+
"""
8+
9+
import matplotlib.pyplot as plt
10+
import numpy as np
11+
12+
# %%
13+
14+
# Default behaviour
15+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
16+
ax.pcolormesh(np.random.randn(100, 100))
17+
18+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
19+
ax.pcolormesh(np.random.randn(100, 100))
20+
21+
22+
# %%
23+
24+
# sphinx_gallery_multi_image_block = "single"
25+
26+
# Non-default behaviour for just this cell
27+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
28+
ax.pcolormesh(np.random.randn(100, 100))
29+
30+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
31+
ax.pcolormesh(np.random.randn(100, 100))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
Force images to be displayed on separate lines
3+
==============================================
4+
This example demonstrates how the ``sphinx_gallery_multi_image`` option can be used to
5+
force images to be displayed on separate lines, rather than the default behaviour of
6+
displaying them side-by-side.
7+
"""
8+
9+
# sphinx_gallery_multi_image = "single"
10+
11+
# %%
12+
13+
import matplotlib.pyplot as plt
14+
import numpy as np
15+
16+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
17+
ax.pcolormesh(np.random.randn(100, 100))
18+
19+
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
20+
ax.pcolormesh(np.random.randn(100, 100))

0 commit comments

Comments
 (0)
0