8000 Sphinx extension: support captions in inline plots. · matplotlib/matplotlib@99e2e27 · GitHub
[go: up one dir, main page]

Skip to content
.hEHvLI{min-width:0;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/ .bmcJak{min-width:0;}/*!sc*/ .fyKNMY[data-size="medium"]{color:var(--fgColor-default,var(--color-fg-default,#1F2328));}/*!sc*/ .gUkoLg{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;}/*!sc*/ .dpBUfI{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/ @media screen and (min-width:544px){.dpBUfI{-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}}/*!sc*/ @media screen and (min-width:768px){.dpBUfI{-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}}/*!sc*/ @media screen and (min-width:1012px){.dpBUfI{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;}}/*!sc*/ .hKWjvQ{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/ .cvdqJW{width:20px;height:20px;margin-right:8px;margin-top:-1px;margin-left:1px;}/*!sc*/ .dkaFxu{font-weight:600;white-space:nowrap;color:var(--fgColor-default,var(--color-fg-default,#1F2328));}/*!sc*/ .dkaFxu:hover{color:var(--fgColor-default,var(--color-fg-default,#1F2328));-webkit-text-decoration:underline;text-decoration:underline;}/*!sc*/ .irPhWZ{width:60px;}/*!sc*/ .dNbsEP{width:62px;}/*!sc*/ .kHfwUD{width:60px;height:22px;}/*!sc*/ .bHLmSv{position:absolute;inset:0 -2px;cursor:col-resize;background-color:transparent;-webkit-transition-delay:0.1s;transition-delay:0.1s;}/*!sc*/ .bHLmSv:hover{background-color:var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2)));}/*!sc*/ .hqtbbn{bottom:0 !important;-webkit-clip:rect(1px,1px,1px,1px);clip:rect(1px,1px,1px,1px);-webkit-clip-path:inset(50%);clip-path:inset(50%);height:84px;position:absolute;width:320px;}/*!sc*/ data-styled.g1[id="Box-sc-g0xbh4-0"]{content:"hEHvLI,bmcJak,fyKNMY,gUkoLg,dpBUfI,hKWjvQ,cvdqJW,dkaFxu,irPhWZ,dNbsEP,kHfwUD,bHLmSv,hqtbbn,"}/*!sc*/ .brGdpi{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;-webkit-clip:rect(0,0,0,0);clip:rect(0,0,0,0);white-space:nowrap;border-width:0;}/*!sc*/ data-styled.g4[id="_VisuallyHidden__VisuallyHidden-sc-11jhm7a-0"]{content:"brGdpi,"}/*!sc*/ .jjwhNb{position:relative;display:inline-block;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}/*!sc*/ .jjwhNb::after{position:absolute;z-index:1000000;display:none;padding:0.5em 0.75em;font:normal normal 11px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";-webkit-font-smoothing:subpixel-antialiased;color:var(--tooltip-fgColor,var(--fgColor-onEmphasis,var(--color-fg-on-emphasis,#ffffff)));text-align:center;-webkit-text-decoration:none;text-decoration:none;text-shadow:none;text-transform:none;-webkit-letter-spacing:normal;-moz-letter-spacing:normal;-ms-letter-spacing:normal;letter-spacing:normal;word-wrap:break-word;white-space:pre;pointer-events:none;content:attr(aria-label);background:var(--tooltip-bgColor,var(--bgColor-emphasis,var(--color-neutral-emphasis-plus,#24292f)));border-radius:6px;opacity:0;}/*!sc*/ @-webkit-keyframes tooltip-appear{from{opacity:0;}to{opacity:1;}}/*!sc*/ @keyframes tooltip-appear{from{opacity:0;}to{opacity:1;}}/*!sc*/ .jjwhNb:hover::after,.jjwhNb:active::after,.jjwhNb:focus::after,.jjwhNb:focus-within::after{display:inline-block;-webkit-text-decoration:none;text-decoration:none;-webkit-animation-name:tooltip-appear;animation-name:tooltip-appear;-webkit-animation-duration:0.1s;animation-duration:0.1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;-webkit-animation-delay:0s;animation-delay:0s;}/*!sc*/ .jjwhNb.tooltipped-no-delay:hover::after,.jjwhNb.tooltipped-no-delay:active::after,.jjwhNb.tooltipped-no-delay:focus::after,.jjwhNb.tooltipped-no-delay:focus-within::after{-webkit-animation-delay:0s;animation-delay:0s;}/*!sc*/ .jjwhNb.tooltipped-multiline:hover::after,.jjwhNb.tooltipped-multiline:active::after,.jjwhNb.tooltipped-multiline:focus::after,.jjwhNb.tooltipped-multiline:focus-within::after{display:table-cell;}/*!sc*/ .jjwhNb.tooltipped-s::after,.jjwhNb.tooltipped-se::after,.jjwhNb.tooltipped-sw::after{top:100%;right:50%;margin-top:6px;}/*!sc*/ .jjwhNb.tooltipped-se::after{right:auto;left:50%;margin-left:-16px;}/*!sc*/ .jjwhNb.tooltipped-sw::after{margin-right:-16px;}/*!sc*/ .jjwhNb.tooltipped-n::after,.jjwhNb.tooltipped-ne::after,.jjwhNb.tooltipped-nw::after{right:50%;bottom:100%;margin-bottom:6px;}/*!sc*/ .jjwhNb.tooltipped-ne::after{right:auto;left:50%;margin-left:-16px;}/*!sc*/ .jjwhNb.tooltipped-nw::after{margin-right:-16px;}/*!sc*/ .jjwhNb.tooltipped-s::after,.jjwhNb.tooltipped-n::after{-webkit-transform:translateX(50%);-ms-transform:translateX(50%);transform:translateX(50%);}/*!sc*/ .jjwhNb.tooltipped-w::after{right:100%;bottom:50%;margin-right:6px;-webkit-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%);}/*!sc*/ .jjwhNb.tooltipped-e::after{bottom:50%;left:100%;margin-left:6px;-webkit-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%);}/*!sc*/ .jjwhNb.tooltipped-multiline::after{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:250px;word-wrap:break-word;white-space:pre-line;border-collapse:separate;}/*!sc*/ .jjwhNb.tooltipped-multiline.tooltipped-s::after,.jjwhNb.tooltipped-multiline.tooltipped-n::after{right:auto;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);}/*!sc*/ .jjwhNb.tooltipped-multiline.tooltipped-w::after,.jjwhNb.tooltipped-multiline.tooltipped-e::after{right:100%;}/*!sc*/ .jjwhNb.tooltipped-align-right-2::after{right:0;margin-right:0;}/*!sc*/ .jjwhNb.tooltipped-align-left-2::after{left:0;margin-left:0;}/*!sc*/ data-styled.g5[id="Tooltip__TooltipBase-sc-17tf59c-0"]{content:"jjwhNb,"}/*!sc*/ .irithh{position:relative;overflow:hidden;-webkit-mask-image:radial-gradient(white,black);mask-image:radial-gradient(white,black);background-color:var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5)));border-radius:3px;display:block;height:1.2em;width:60px;}/*!sc*/ .irithh::after{-webkit-animation:crVFvv 1.5s infinite linear;animation:crVFvv 1.5s infinite linear;background:linear-gradient(90deg,transparent,var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))),transparent);content:'';position:absolute;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);bottom:0;left:0;right:0;top:0;}/*!sc*/ .ihfxfT{position:relative;overflow:hidden;-webkit-mask-image:radial-gradient(white,black);mask-image:radial-gradient(white,black);background-color:var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5)));border-radius:3px;display:block;height:1.2em;width:62px;}/*!sc*/ .ihfxfT::after{-webkit-animation:crVFvv 1.5s infinite linear;animation:crVFvv 1.5s infinite linear;background:linear-gradient(90deg,transparent,var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))),transparent);content:'';position:absolute;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);bottom:0;left:0;right:0;top:0;}/*!sc*/ .kRBfod{position:relative;overflow:hidden;-webkit-mask-image:radial-gradient(white,black);mask-image:radial-gradient(white,black);background-color:var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5)));border-radius:3px;display:block;height:1.2em;width:60px;height:22px;}/*!sc*/ .kRBfod::after{-webkit-animation:crVFvv 1.5s infinite linear;animation:crVFvv 1.5s infinite linear;background:linear-gradient(90deg,transparent,var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))),transparent);content:'';position:absolute;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);bottom:0;left:0;right:0;top:0;}/*!sc*/ data-styled.g27[id="LoadingSkeleton-sc-695d630a-0"]{content:"irithh,ihfxfT,kRBfod,"}/*!sc*/ @-webkit-keyframes crVFvv{0%{-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);}50%{-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%);}100%{-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%);}}/*!sc*/ @keyframes crVFvv{0%{-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);}50%{-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%);}100%{-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%);}}/*!sc*/ data-styled.g53[id="sc-keyframes-crVFvv"]{content:"crVFvv,"}/*!sc*/

Commit 99e2e27

Browse files
committed
Sphinx extension: support captions in inline plots.
This commit adds a :caption: option to the plot directive provided by the Sphinx extension (matplotlib.sphinxext.plot_directive). Without this option, there is no way to specify a caption for a plot generated from inline content. This is fully backwards-compatible. If a plot directive with a path to a source file has both a :caption: option and content provided, the content is used for the caption and the option is ignored.
1 parent 030157c commit 99e2e27

File tree

5 files changed

+230
-1
lines changed

5 files changed

+230
-1
lines changed

lib/matplotlib/sphinxext/plot_directive.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@
1818
1919
This is the caption for the plot
2020
21+
Alternatively, the caption may be given using the :caption: option::
22+
23+
.. plot:: path/to/plot.py
24+
:caption: This is the caption for the plot
25+
26+
If content is given, then the :caption: option is ignored::
27+
28+
.. plot:: path/to/plot.py
29+
:caption: This caption is not used
30+
31+
This is the actual caption used for the plot
32+
2133
Additionally, one may specify the name of a function to call (with
2234
no arguments) immediately after importing the module::
2335
@@ -33,6 +45,17 @@
3345
img = mpimg.imread('_static/stinkbug.png')
3446
imgplot = plt.imshow(img)
3547
48+
To add a caption to an inline plot, the :caption: option must be used::
49+
50+
.. plot::
51+
:caption: This is the caption for the plot.
52+
53+
import matplotlib.pyplot as plt
54+
import matplotlib.image as mpimg
55+
import numpy as np
56+
img = mpimg.imread('_static/stinkbug.png')
57+
imgplot = plt.imshow(img)
58+
3659
3. Using **doctest** syntax::
3760
3861
.. plot::
@@ -70,6 +93,11 @@
7093
If specified, the code block will be run, but no figures will be
7194
inserted. This is usually useful with the ``:context:`` option.
7295
96+
caption : str
97+
If given, the caption to add to the plot. If the code to generate the
98+
plot is specified by a external file and the directive has content,
99+
then this option is ignored.
100+
73101
Additionally, this directive supports all of the options of the `image`
74102
directive, except for *target* (since plot will add its own target). These
75103
include `alt`, `height`, `width`, `scale`, `align` and `class`.
@@ -252,6 +280,7 @@ class PlotDirective(Directive):
252280
'context': _option_context,
253281
'nofigs': directives.flag,
254282
'encoding': directives.encoding,
283+
'caption': directives.unchanged,
255284
}
256285

257286
def run(self):
@@ -666,6 +695,11 @@ def run(arguments, content, options, state_machine, state, lineno):
666695
function_name = None
667696
caption = ''
668697

698+
# We didn't get a caption from the directive content.
699+
# See if the options contains one.
700+
if not caption:
701+
caption = options.get('caption', '')
702+
669703
base, source_ext = os.path.splitext(output_base)
670704
if source_ext in ('.py', '.rst', '.txt'):
671705
output_base = base
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
extensions = ['matplotlib.sphinxext.plot_directive']
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Plot directive caption tests
2+
============================
3+
4+
Inline plot with no caption:
5+
6+
.. plot::
7+
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
f = 3
11+
t = np.linspace(0, 1, 100)
12+
s = np.sin(2 * np.pi * f * 9E88 t)
13+
plt.plot(t, s)
14+
15+
Inline plot with caption:
16+
17+
.. plot::
18+
:caption: Caption for inline plot.
19+
20+
import matplotlib.pyplot as plt
21+
import numpy as np
22+
f = 3
23+
t = np.linspace(0, 1, 100)
24+
s = np.sin(2 * np.pi * f * t)
25+
plt.plot(t, s)
26+
27+
Included file with no caption:
28+
29+
.. plot:: test_plot.py
30+
31+
Included file with caption in the directive content:
32+
33+
.. plot:: test_plot.py
34+
35+
This is a caption in the content.
36+
37+
Included file with caption option:
38+
39+
.. plot:: test_plot.py
40+
:caption: This is a caption in the options.
41+
42+
If both content and options have a caption, the one in the content should prevail:
43+
44+
.. plot:: test_plot.py
45+
:caption: This should be ignored.
46+
47+
The content caption should be used instead.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
f = 3
4+
t = np.linspace(0, 1, 100)
5+
s = np.sin(2 * np.pi * f * t)
6+
plt.plot(t, s)

lib/matplotlib/tests/test_sphinxext.py

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,155 @@
22

33
import filecmp
44
from os.path import join as pjoin, dirname, isdir
5+
import pathlib
56
from subprocess import Popen, PIPE
67
import sys
8+
import warnings
79

810
import pytest
911

10-
12+
# Only run the tests if Sphinx is installed.
1113
pytest.importorskip('sphinx')
1214

15+
# Docutils is a dependency of Sphinx so it is safe to
16+
# import after we know Sphinx is available.
17+
from docutils.nodes import caption, figure
18+
19+
# Sphinx has some deprecation warnings we don't want to turn into errors.
20+
with warnings.catch_warnings():
21+
warnings.simplefilter('ignore')
22+
from sphinx.application import Sphinx
23+
24+
25+
#: Directory of sources for testing the Sphinx extension.
26+
SRCDIR = pathlib.Path(__file__).parent / 'sphinxext_sources'
27+
28+
29+
class NodeFilter:
30+
"""Test utility class to filter nodes from a Sphinx doctree.
31+
32+
This is designed to be used with the walkabout() method of nodes. You
33+
probably want to use the filter_children() class method.
34+
35+
Parameters
36+
----------
37+
document : node
38+
The document node.
39+
classes : list of classes
40+
The node classes to filter from the document. If None, all classes will
41+
be accepted resulting in a flattened list of all nodes.
42+
43+
"""
44+
def __init__(self, document, classes=None):
45+
self.document = document
46+
self.nodes = []
47+
if classes:
48+
self.classes = tuple(classes)
49+
else:
50+
self.classes = None
51+
52+
def dispatch_visit(self, obj):
53+
if not self.classes or isinstance(obj, self.classes):
54+
self.nodes.append(obj)
55+
56+
def dispatch_departure(self, obj):
57+
pass
58+
59+
@classmethod
60+
def filter_children(cls, document, parent, classes=None):
61+
"""Filter child nodes from a parent node.
62+
63+
Parameters
64+
----------
65+
document : node
66+
The main document node.
67+
parent : node
68+
The parent node to work on.
69+
classes : list of classes
70+
The node classes to filter.
71+
72+
Returns
73+
-------
74+
children : list
75+
A list of the nodes which are instances of the given classes or
76+
their subclasses.
77+
78+
"""
79+
obj = cls(document, classes=classes)
80+
parent.walkabout(obj)
81+
return obj.nodes
82+
83+
84+
def build_test_doc(src_dir, build_dir, builder='html'):
85+
"""Build a test document.
86+
87+
Parameters
88+
----------
89+
src_dir : pathlib.Path
90+
The location of the sources.
91+
build_dir : pathlib.Path
92+
The build directory to use.
93+
builder : str
94+
Which builder to use.
95+
96+
Returns
97+
-------
98+
app : sphinx.application.Sphinx
99+
The Sphinx application that built the document.
100+
101+
"""
102+
doctree_dir = build_dir / "doctrees"
103+
output_dir = build_dir / "html"
104+
105+
# Avoid some internal Sphinx deprecation warnings being turned into errors.
106+
with warnings.catch_warnings():
107+
warnings.simplefilter('ignore')
108+
app = Sphinx(src_dir, src_dir, output_dir, doctree_dir, builder)
109+
app.build()
110+
return app
111+
112+
113+
def test_plot_directive_caption(tmpdir):
114+
"""Test the :caption: option of the plot directive.
115+
116+
"""
117+
# Build the test document.
118+
localsrc = SRCDIR / "plot_directive_caption"
119+
build_dir = pathlib.Path(tmpdir)
120+
app = build_test_doc(localsrc, build_dir)
121+
122+
# Get the main document and filter out the figures in it.
123+
index = app.env.get_doctree('index')
124+
figures = NodeFilter.filter_children(index, index, [figure])
125+
126+
# The captions we expect to find.
127+
expected = [
128+
None,
129+
'Caption for inline plot.',
130+
None,
131+
'This is a caption in the content.',
132+
'This is a caption in the options.',
133+
'The content caption should be used instead.',
134+
]
135+
136+
# N.B., each plot directive generates two figures:
137+
# one HTML only and one for other builders.
138+
assert len(figures) == 2 * len(expected), \
139+
"Wrong number of figures in document."
140+
141+
# Check the caption nodes are correct.
142+
for i, figurenode in enumerate(figures):
143+
n = i // 2
144+
captions = NodeFilter.filter_children(index, figurenode, [caption])
145+
146+
if expected[n]:
147+
assert len(captions) > 0, f"Figure {n+1}: no caption found."
148+
assert len(captions) < 2, f"Figure {n+1}: too many captions."
149+
assert captions[0].astext().strip() == expected[n], \
150+
f"Figure {n+1}: wrong caption"
151+
else:
152+
assert len(captions) == 0, f"Figure {n+1}: unexpected caption."
153+
13154

14155
def test_tinypages(tmpdir):
15156
html_dir = pjoin(str(tmpdir), 'html')
3717

0 commit comments

Comments
 (0)
0