GeezerVison is showing github.com — ENH,DOC: Only render plot directive formats the Sphinx builder can use by agriyakhetarpal · Pull Request #32584 · matplotlib/matplotlib
Conversation
PR summary
This PR makes the .. plot:: directive render only the formats the active Sphinx builder can use. In practice, this means that the Sphinx HTML builder keeps the first entry of the list, and the remaining entries are kept only when plot_html_show_formats is True. This is since the HTML template only reaches those files through the download links it emits in that case.
Every other Sphinx builder keeps the entries whose MIME type appears in its supported_image_types, so a LaTeX/PDF build would get its PDFs, but a texinfo build would not. The MIME type comes from sphinx.util.images.guess_mimetype: https://github.com/sphinx-doc/sphinx/blob/035a07c32c3241fd3356f4d827800c1f88525272/sphinx/util/images.py#L69
Doing this showcases some speedups for projects that set plot_html_show_formats = False and leave plot_formats at its default. Until now, their HTML documentation builds also wrote a PDF and a high-resolution PNG of every figure into the plot_directive build directory, which were left redundant. The rendered output is unchanged across builders. Only the contents of that intermediate directory differ.
In Matplotlib's own doc/conf.py, I noticed that this was being worked around by inspecting the command-line arguments for the build target. Since the directive now does the selection, we include a more principled fix for the same.
This PR is a continuation/generalisation of what @Schefflera-Arboricola, @melissawm, and I recently did for the PyWavelets project, via PyWavelets/pywt#884, to speed up its documentation builds. I primarily found this scope for optimisation via the sphinx-benchmark project, which @Schefflera-Arboricola developed and released as part of her internship at Quansight this year – where @melissawm and I were involved as mentors. All credits go to @melissawm for suggesting that this could possibly be generalised in Matplotlib itself, after which we discussed it, and I took over the change. Also, please read Aditi's blog post; it's a great read! https://labs.quansight.org/blog/sphinx-benchmark.
Through a GitHub search of public conf.py files, mostly coordinated with a Claude Code Opus 5.5 session, I found around 330 projects in the affected configuration, while SciPy, pandas, seaborn, scikit-learn, and several others already avoid the extra cost by pinning plot_formats to PNG by hand.
That said, however, by the means of some wilful cherry-picking (:P), I found speedups for some projects. Here is a benchmark table, via results from hyperfine, with some runs on my MacBook Pro (M3 Pro) if that is useful:
Benchmark results
Each project's documentation website was built with its own pinned requirements and its own build command. The only difference between the two variants is this PR's diff applied to the installed plot_directive.py. To ensure the correctness of this benchmark, I (i) checked patched runs and noticed that they had zero written PDFs, and (ii) I always ran clean builds with no .pyc files or previously-stored doctrees by entirely wiping any built/stored folders with git clean -xdff.
| Project |
Build command |
Stock |
Patched |
Speedup |
Runs |
PDFs skipped |
| PyWavelets |
make html |
33.3 ± 0.8 s |
12.9 ± 0.3 s |
2.59x |
5 |
13 MB |
| data-morph |
make html |
17.4 ± 0.3 s |
10.6 ± 0.2 s |
1.64x |
5 |
86 files, 1.2 MB |
| getdist |
sphinx-build -b html |
13.0 ± 1.4 s |
8.3 ± 0.2 s |
1.57x |
5 |
14 files, 7.1 MB |
| torchoptics |
make html |
33.1 ± 0.9 s |
23.7 ± 0.6 s |
1.39x |
3 |
51 files, 13.2 MB |
| qutip |
make html |
60.5 ± 0.5 s |
53.6 ± 1.0 s |
1.13x |
5 |
93 files, 10.7 MB |
| Qiskit |
tox -e docs command |
34.6 ± 0.4 s |
31.2 ± 0.3 s |
1.11x |
3 |
141 files, 2 MB |
| NumPy |
spin docs |
124.3 ± 0.7 s |
no change |
1.00x |
5 |
144 files, 2.1 MB |
Here, the savings track how expensive the figures are to render as PDF. We do not count how many figures there are or how large the files end up after being built.
- In PyWavelets, we have a handful of large
pcolormesh figures.
- In
getdist, there are a number of grids drawn of filled contours.
- Despite the small files, Qiskit's cost comes from 3D Bloch spheres, which were expensive to build.
- In NumPy, the line plots were fast enough that nothing changed within noise, unfortunately.
- and so on
AI Disclosure
My search of public GitHub projects was coordinated with a Claude Code Opus 5.5 session. I prepared an initial draft of the changes to the directive. Through a review, the agent found Sphinx's util.guess_mimetypes method for me, which I did not know about—I was hardcoding a map of file formats and their MIME types initially—since the method is not really documented as such in the Sphinx documentation website, but it is still de facto part of the public API.
I also consulted this session to quickly set up the projects for development by following their developer guides and installing their dependencies as documented by them, and setting up the benchmarking infrastructure required on my machine between runs. I provided instructions to the agent on how to do so reliably via hyperfine, including the commands. I have provided them below if you would like to reproduce the results, working with your own agent:
Benchmarking instructions and how to reproduce
For any project using the plot directive:
# 1. A venv with the project's own docs requirements, plus hyperfine from your package manager (I use Homebrew on my macOS).
uv venv venv && uv pip install --python venv/bin/python -r <docs requirements>
# 2. Two copies of the installed directive, one which is the stock, and one, which is with this PR applied.
SITE=$(venv/bin/python -c "import matplotlib.sphinxext.plot_directive as m; print(m.__file__)")
cp "$SITE" plot_directive.stock.py
cp "$SITE" plot_directive.patched.py
git -C <matplotlib checkout> diff upstream/main -- lib/matplotlib/sphinxext/plot_directive.py > pd.diff
patch -p3 plot_directive.patched.py < pd.diff
# 3. Time both documentation builds, swapping the module in before each variant and cleaning before each run.
PATH=$PWD/venv/bin:$PATH hyperfine --runs 5 --warmup 1 \
-L variant stock,patched \
--setup "cp $PWD/plot_directive.{variant}.py $SITE" \
--prepare '<the project's clean command, such as make clean>' \
'<the project's HTML build command>'
# 4. Afterwards, confirm that the patched variant wrote no PDFs, and restore the stock module.
find <build dir>/plot_directive -name '*.pdf' | wc -l
cp plot_directive.stock.py "$SITE"
Per project, we found that PyWavelets, qutip, and data-morph used make html with make clean. getdist used sphinx-build -T -b html docs/source docs/_build/html with rm -rf docs/_build. Qiskit used sphinx-build in its tox.ini docs environment without -W, after a one-time make cheader and doxygen docs/Doxyfile, with rm -rf docs/_build. NumPy used spin docs with SPHINXOPTS unset to avoid -W, removing doc/build and the generated reference directories between runs.
PR quality check
- Use an expressive title, e.g. "Fix title font property precedence"
- New and changed code is tested
- [N/A] Plotting related features are demonstrated in an example
- New features and API changes have release notes
- Documentation complies with general and docstring guidelines
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
story645, Schefflera-Arboricola, and melissawm reacted with heart emoji
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this is really a great finding! 🚀
One question for understanding: Is the following correct?
Before we just rendered every format in plot_formats and the renderer just picked the suitable one - and exactly only one. In practice a user would put all formats they need in plot_format.
Exception: With plot_html_show_formats the HTML builder uses all formats because it embeds them as links.
This is inefficient as more files are created than needed.
Also, we may have overlapping concerns in plot_formats: Say I want png and svg in HTML with plot_html_show_formats, but I also want latex, which needs pdf - that pdf is inadvertedly also picked up by the HTML reader.
The proposed solution is to subset plot_formats with the supported formats of the builder.
Wouldn't it be cleaner to turn plot_formats into a per-builder dict? i.e.
default_plot_formats = {
'html': ['png'],
'latex': ['pdf'],
...
}
and
- if
plot_format is given and a dict, use plot_format[builder]
- if
plot_format is given and not a dict, use plot_format as is (backward compatibility - maybe warn?
- otherwise use
default_plot_formats[builder]
Optionally, warn if the formats contain a file that is not supported by the builder.
Advantage: There's less magic in selecting the files, configuration is clearer and can be explicitly done per builder.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Hi @timhoffm, thanks for the feedback!
One question for understanding: Is the following correct?
Before we just rendered every format in plot_formats and the renderer just picked the suitable one - and exactly only one. In practice a user would put all formats they need in plot_format. Exception: With plot_html_show_formats the HTML builder uses all formats because it embeds them as links.
Yes, this is correct!
A per-builder dict seems sensible to me, too, so I added that in 8a12b54, along with a warning for unsupported file formats in 5b9a15b. Could you PTAL when you have a chance?
I kept the default as the current list instead of the dict, because there are currently many projects using a list (see code search results, though there is a lot of noise too), and I think it might be better to not bother them with a warning :D The dict is purely additive, so that projects can switch to it at their convenience. However, I can still add a warning suggesting they switch their lists to a dict if you insist.
Additionally, I asked Claude Code to review my diff, and it caught a latent regression that the PR's behaviour surfaced, namely that we now need to record which documents contain plot directives. This is because it found that a PDF build which ran after an HTML build, i.e., with a reused doctree directory, failed to find previously generated PDFs (when we were building all formats, prior to this PR), and as a result ended up embedding low-quality raster PNGs instead of vector PDF figures into the PDF output. This is the case, for example, for users building documentation with plot directives on Read the Docs with PDF outputs enabled, since builds there rely on such caching. I have fixed this in 9c07cff.
Thanks again!
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
timhoffm
commented
Nov 10, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
There's a decent amount of complexity here. I'll try to think this through from scratch, because I feel incrementally attaching more and more logic makes this hard to comprehend.
Simple/standard case: one build
-
Depending on the builder, we may need no, one or multiple files for each plot. Absent user configuration, we'd want to provide builder-specific defaults. A dict is a reasonable representation:
DEFAULT_PLOT_FORMATS = {
'html': ['png', 'pdf'],
'latex': ['pdf'],
'text': [],
...
}
Most builders will only need one, but html is the prominent exception, which motivates the more general approach.
Discarded alternative
I briefly thought that it may be better to attach the default plot format to the builder - this would third-party builders give the option to specify default formats. But that doesn't work. We don't have authority to attach an attribute to the builtin sphinx builders, so the default configuration has to live in the plot_directive extension.
Details
-
Users should be able to override them, using the same dict format in conf.py
plot_formats = {
'html': ['svg'],
}
-
Third-party builders are not in DEFAULT_PLOT_FORMATS. We should decide what to do in that case - either render nothing or raise or for backward-compatibility the current default. I'd go with the current default: ('png', 'hires.png', 'pdf'). We may later deprecate this and decide to choose one of the other alternatives.
In general, users of third-party builders may need to specify their builder in plot_formats to get proper output. We can't magically guess what the builder needs.
-
Backward compatibility for explicitly set plot_formats lists: Such a list would take precedence over DEFAULT_PLOT_FORMATS - effectively as if one had set plot_formats = {setup.app.builder: explicit_list}.
This usage is discouraged but currently not deprecated.
Validation
For the selected builder, it should be verified that plot_formats[builder] only contains supported formats.
We may later decide whether a pure list should be checked as well. - Possibly not right now.
Multiple builders
The cache reuse across builders complicates the situation. I can see two approaches:
a. as proposed above, track the files and build missing files in the second run.
b. alternatively, add an expert option to generate plots for more builders than only the current one. Advantage is that we don't need the complexity of tracking files, and saving to multiple output formats is faster. If all images can be generated in the first build, we only create the figure once and call savefig multiple times. The second buider can then rely on the cache. In contrast, generating additional image files in the second builder on demand can be substantially more costly. We have to recreate the whole figure.
I'm undecided here. (a) is a bit cleaner, but more complex and costly. (b) would be as simple as an optional plot_for_builder = ["html", "latex"] (or most likely -Dplot_for_builder=html,latex, because you'd typlically run this in scripts.
This would save the union of the plot_formats of all given builders.
The cache key would then simply be the set of saved formats. And the cache-usage criterion would be whether the requested formats is a subset of the saved formats.
I'm leaning towards (b)
What do you think?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This branch has not been deployed
No deployments
Reviewers
timhoffm
Awaiting requested review from timhoffm
melissawm
Awaiting requested review from melissawm
At least 1 approving review is required to merge this pull request.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|